File Coverage

blib/lib/Modern/OpenAPI/Generator/CodeGen/Server.pm
Criterion Covered Total %
statement 53 65 81.5
branch 7 8 87.5
condition 6 24 25.0
subroutine 11 13 84.6
pod 2 2 100.0
total 79 112 70.5


line stmt bran cond sub pod time code
1             package Modern::OpenAPI::Generator::CodeGen::Server;
2              
3 6     6   59 use v5.26;
  6         19  
4 6     6   24 use strict;
  6         9  
  6         134  
5 6     6   16 use warnings;
  6         9  
  6         219  
6 6     6   20 use Carp qw(croak);
  6         10  
  6         265  
7 6     6   2684 use YAML::PP ();
  6         303875  
  6         4752  
8              
9             sub generate {
10 4     4 1 25 my ( $class, %arg ) = @_;
11 4   33     46 my $writer = $arg{writer} // croak 'writer';
12 4   33     14 my $spec = $arg{spec} // croak 'spec';
13 4   33     13 my $base = $arg{base} // croak 'base';
14 4   50     14 my $skeleton = $arg{skeleton} // 0;
15 4   50     11 my $local_test = $arg{local_test} // 0;
16 4   50     16 my $ui = $arg{ui} // 1;
17              
18 4         15 my $lib = 'lib/' . _pathify_dir($base);
19              
20 4         18 my $augmented = $spec->clone_with_mojo_to('Controller');
21 4         32 my $yaml = YAML::PP->new( boolean => 'JSON::PP' )->dump_string($augmented);
22 4         48184 $writer->write( 'share/openapi.mojo.yaml', $yaml );
23              
24 4         10 my $server_pkg = "$base\::Server";
25              
26 4         21 $writer->write( "$lib/Server.pm", _server_pm_api( $server_pkg, $base, $ui ) );
27 4         28 $writer->write(
28             "$lib/Server/Controller.pm",
29             _controller_pm( "$base\::Server::Controller", $base, $spec, $spec->operations,
30             $skeleton, $local_test )
31             );
32 4         32 $writer->write( 'script/server.pl', _script_pm($base) );
33             }
34              
35             # Same HTTP server entrypoint, but only serves share/openapi.yaml + Swagger UI (no REST API from spec).
36             sub generate_spec_ui_only {
37 0     0 1 0 my ( $class, %arg ) = @_;
38 0   0     0 my $writer = $arg{writer} // croak 'writer';
39 0   0     0 my $spec = $arg{spec} // croak 'spec';
40 0   0     0 my $base = $arg{base} // croak 'base';
41              
42 0         0 my $lib = 'lib/' . _pathify_dir($base);
43 0         0 my $title = $spec->title;
44 0         0 $title =~ s/'/\\'/g;
45              
46 0         0 my $server_pkg = "$base\::Server";
47 0         0 $writer->write( "$lib/Server.pm", _server_pm_spec_only( $server_pkg, $title ) );
48 0         0 $writer->write( 'script/server.pl', _script_pm($base) );
49             }
50              
51             sub _pathify_dir {
52 4     4   9 my ($name) = @_;
53 4         20 $name =~ s{::}{/}g;
54 4         11 return $name;
55             }
56              
57             sub _safe_operation_sub {
58 4     4   9 my ($oid) = @_;
59 4         17 $oid =~ s/[^A-Za-z0-9_]/_/g;
60 4         7 return $oid;
61             }
62              
63             # Full API (Mojolicious::Plugin::OpenAPI) + optional Swagger UI at /swagger
64             sub _server_pm_api {
65 4     4   13 my ( $pkg, $base, $ui ) = @_;
66 4 100       12 if ($ui) {
67 3         43 return <<"PM";
68             package $pkg;
69              
70             use v5.26;
71             use strict;
72             use warnings;
73              
74             use Mojo::Base 'Mojolicious', -signatures;
75             use Storable qw(dclone);
76              
77             sub startup (\$self) {
78             \$self->routes->namespaces(['$base\::Server']);
79             my \$spec = \$self->home->child('share', 'openapi.mojo.yaml');
80             \$self->plugin(
81             OpenAPI => {
82             url => \$spec->to_string,
83             route => \$self->routes,
84             }
85             );
86             \$self->helper(
87             openapi_yaml_for_swagger_ui => sub (\$c) {
88             state \$spec_data;
89             if ( !\$spec_data ) {
90             require YAML::PP;
91             my \$p = \$c->app->home->child( 'share', 'openapi.mojo.yaml' );
92             \$spec_data = YAML::PP->new( boolean => 'JSON::PP' )->load_file("\$p");
93             }
94             my \$doc = dclone(\$spec_data);
95             if ( \$ENV{OAPI_SWAGGER_LOCAL_ORIGIN} ) {
96             my \$u = \$c->req->url->to_abs->clone;
97             \$u->path('/');
98             \$u->query(undef);
99             my \$origin = \$u->to_string;
100             \$origin =~ s{/\\z}{};
101             my \$srv = \$doc->{servers};
102             \$srv = [] unless ref \$srv eq 'ARRAY';
103             my \$dup = 0;
104             if (@\$srv) {
105             my \$f = \$srv->[0];
106             if ( ref \$f eq 'HASH' ) {
107             ( my \$x = \$f->{url} // '' ) =~ s{/\\z}{};
108             \$dup = 1 if \$x eq \$origin;
109             }
110             }
111             unless (\$dup) {
112             unshift @\$srv,
113             {
114             url => \$origin,
115             description => 'This server (request origin)',
116             };
117             }
118             \$doc->{servers} = \$srv;
119             }
120             require YAML::PP;
121             my \$yaml = YAML::PP->new( boolean => 'JSON::PP' )->dump_string(\$doc);
122             \$c->res->headers->content_type('application/yaml; charset=UTF-8');
123             \$c->render( text => \$yaml );
124             }
125             );
126             \$self->routes->get(
127             '/openapi.yaml' => sub (\$c) {
128             \$c->openapi_yaml_for_swagger_ui;
129             }
130             );
131             \$self->plugin(
132             SwaggerUI => {
133             route => \$self->routes->any('/swagger'),
134             url => '/openapi.yaml',
135             }
136             );
137             }
138              
139             1;
140             PM
141             }
142              
143 4         7 return <<"PM";
144             package $pkg;
145              
146             use v5.26;
147             use strict;
148             use warnings;
149              
150             use Mojo::Base 'Mojolicious', -signatures;
151              
152             sub startup (\$self) {
153             \$self->routes->namespaces(['$base\::Server']);
154             my \$spec = \$self->home->child('share', 'openapi.mojo.yaml');
155             \$self->plugin(
156             OpenAPI => {
157             url => \$spec->to_string,
158             route => \$self->routes,
159             }
160             );
161             }
162              
163             1;
164             PM
165             }
166              
167             # Only spec file + Swagger UI (e.g. --no-server --ui is not used; this is --no-server with ui)
168             sub _server_pm_spec_only {
169 0     0   0 my ( $pkg, $title ) = @_;
170 0         0 return <<"PM";
171             package $pkg;
172              
173             use v5.26;
174             use strict;
175             use warnings;
176              
177             use Mojo::Base 'Mojolicious', -signatures;
178             use Storable qw(dclone);
179              
180             sub startup (\$self) {
181             \$self->helper(
182             openapi_yaml_for_swagger_ui => sub (\$c) {
183             state \$spec_data;
184             if ( !\$spec_data ) {
185             require YAML::PP;
186             my \$p = \$c->app->home->child( 'share', 'openapi.yaml' );
187             \$spec_data = YAML::PP->new( boolean => 'JSON::PP' )->load_file("\$p");
188             }
189             my \$doc = dclone(\$spec_data);
190             if ( \$ENV{OAPI_SWAGGER_LOCAL_ORIGIN} ) {
191             my \$u = \$c->req->url->to_abs->clone;
192             \$u->path('/');
193             \$u->query(undef);
194             my \$origin = \$u->to_string;
195             \$origin =~ s{/\\z}{};
196             my \$srv = \$doc->{servers};
197             \$srv = [] unless ref \$srv eq 'ARRAY';
198             my \$dup = 0;
199             if (@\$srv) {
200             my \$f = \$srv->[0];
201             if ( ref \$f eq 'HASH' ) {
202             ( my \$x = \$f->{url} // '' ) =~ s{/\\z}{};
203             \$dup = 1 if \$x eq \$origin;
204             }
205             }
206             unless (\$dup) {
207             unshift @\$srv,
208             {
209             url => \$origin,
210             description => 'This server (request origin)',
211             };
212             }
213             \$doc->{servers} = \$srv;
214             }
215             require YAML::PP;
216             my \$yaml = YAML::PP->new( boolean => 'JSON::PP' )->dump_string(\$doc);
217             \$c->res->headers->content_type('application/yaml; charset=UTF-8');
218             \$c->render( text => \$yaml );
219             }
220             );
221             \$self->routes->get(
222             '/openapi.yaml' => sub (\$c) {
223             \$c->openapi_yaml_for_swagger_ui;
224             }
225             );
226             \$self->plugin(
227             SwaggerUI => {
228             route => \$self->routes->any('/swagger'),
229             url => '/openapi.yaml',
230             title => '$title',
231             }
232             );
233             }
234              
235             1;
236             PM
237             }
238              
239             sub _controller_pm {
240 4     4   14 my ( $pkg, $base, $spec, $ops, $skeleton, $local_test ) = @_;
241 4 100       15 my $stubdata_use =
242             $local_test ? "use ${base}::StubData;\n" : '';
243 4         6 my @subs;
244 4         8 for my $op (@$ops) {
245 4         16 my $oid = _safe_operation_sub( $op->{operation_id} );
246 4 100       14 if ($local_test) {
247 2         9 ( my $oid_q = $op->{operation_id} ) =~ s/'/\\'/g;
248 2         8 push @subs, <
249             sub $oid {
250             my \$self = shift;
251             return unless \$self->openapi->valid_input;
252             my (\$st, \$body) = ${base}::StubData->for_operation('$oid_q');
253             return \$self->render( status => \$st, json => \$body );
254             }
255             SUB
256             }
257             else {
258 2 50       8 my $todo = $skeleton ? '' : "\n # TODO: implement business logic for $oid\n";
259 2         7 push @subs, <
260             sub $oid {$todo
261             my \$self = shift;
262             return unless \$self->openapi->valid_input;
263             \$self->stash(
264             status => 501,
265             openapi => { errors => [ { message => 'Not implemented', path => '/' } ] },
266             );
267             return \$self->render;
268             }
269             SUB
270             }
271             }
272              
273 4         14 my $body = join "\n", @subs;
274 4         26 return <<"PM";
275             package $pkg;
276              
277             use v5.26;
278             use strict;
279             use warnings;
280              
281             use Mojo::Base 'Mojolicious::Controller', -signatures;
282             $stubdata_use
283             $body
284              
285             1;
286             PM
287             }
288              
289             sub _script_pm {
290 4     4   13 my ($base) = @_;
291 4         18 return <<"SCRIPT";
292             #!/usr/bin/env perl
293             use v5.26;
294             use strict;
295             use warnings;
296             use FindBin qw(\$Bin);
297             use File::Spec;
298             BEGIN {
299             \$ENV{MOJO_HOME} ||= File::Spec->catdir( \$Bin, '..' );
300             unshift \@INC, File::Spec->catdir( \$Bin, '..', 'lib' );
301             if ( grep { \$_ eq '--local-test' } \@ARGV ) {
302             \$ENV{OAPI_SWAGGER_LOCAL_ORIGIN} = 1;
303             \@ARGV = grep { \$_ ne '--local-test' } \@ARGV;
304             }
305             }
306             use ${base}::Server;
307             ${base}::Server->new->start(\@ARGV);
308             SCRIPT
309             }
310              
311             1;
312              
313             __END__