File Coverage

blib/lib/Modern/OpenAPI/Generator/CodeGen/Client.pm
Criterion Covered Total %
statement 83 85 97.6
branch 12 22 54.5
condition 10 26 38.4
subroutine 14 14 100.0
pod 1 1 100.0
total 120 148 81.0


line stmt bran cond sub pod time code
1             package Modern::OpenAPI::Generator::CodeGen::Client;
2              
3 6     6   52 use v5.26;
  6         17  
4 6     6   19 use strict;
  6         8  
  6         102  
5 6     6   22 use warnings;
  6         8  
  6         213  
6 6     6   21 use Carp qw(croak);
  6         7  
  6         274  
7 6     6   2929 use Data::Dumper ();
  6         40574  
  6         6705  
8              
9             sub generate {
10 3     3 1 16 my ( $class, %arg ) = @_;
11 3   33     11 my $writer = $arg{writer} // croak 'writer';
12 3   33     9 my $spec = $arg{spec} // croak 'spec';
13 3   33     9 my $base = $arg{base} // croak 'base';
14 3   50     9 my $sync = $arg{sync} // 1;
15 3   50     11 my $async = $arg{async} // 1;
16 3   50     10 my $sigs = $arg{signatures} // [];
17              
18 3         13 my $ops = $spec->operations;
19 3         15 my $lib = 'lib/' . _pathify_dir($base);
20              
21 3         6 my $core_pkg = "$base\::Client::Core";
22 3         5 my $ops_pkg = "$base\::Client::Ops";
23 3         6 my $sync_pkg = "$base\::Client::Sync";
24 3         5 my $async_pkg = "$base\::Client::Async";
25              
26 3         13 $writer->write( "$lib/Client/Core.pm", _core_pm( $core_pkg, $base, $sigs ) );
27 3         17 $writer->write( "$lib/Client/Ops.pm",
28             _ops_pm( $ops_pkg, $core_pkg, $ops, $sync, $async, $spec, $base ) );
29 3 50       33 $writer->write( "$lib/Client/Sync.pm", _sync_pm( $sync_pkg, $ops_pkg, $core_pkg ) ) if $sync;
30 3 50       23 $writer->write( "$lib/Client/Async.pm", _async_pm( $async_pkg, $ops_pkg, $core_pkg ) ) if $async;
31             }
32              
33             sub _pathify_dir {
34 3     3   6 my ($name) = @_;
35 3         14 $name =~ s{::}{/}g;
36 3         7 return $name;
37             }
38              
39             sub _sanitize_sub {
40 3     3   44 my ($s) = @_;
41 3         51 $s =~ s{([a-z])([A-Z])}{$1_$2}g;
42 3         19 $s =~ s{[^A-Za-z0-9]+}{_}g;
43 3         12 return lc $s;
44             }
45              
46             sub _response_model_pkg {
47 3     3   5 my ( $spec, $base, $op ) = @_;
48 3 50       12 my $ref = $op->{response_schema_ref} or return '';
49 3 50       30 my ($name) = $ref =~ m{\#/components/schemas/([^/]+)\z} or return '';
50 3         13 my $sch = $spec->raw->{components}{schemas}{$name};
51 3 50       10 return '' unless ref $sch eq 'HASH';
52 3 50 33     27 return '' if $sch->{allOf} || $sch->{oneOf} || $sch->{anyOf};
      33        
53 3 50 50     23 return '' unless ( $sch->{type} // '' ) eq 'object' && ref $sch->{properties} eq 'HASH';
      33        
54 3         8 ( my $safe = $name ) =~ s/[^A-Za-z0-9_]/_/g;
55 3         10 return "$base\::Model::$safe";
56             }
57              
58             sub _meta_literal {
59 3     3   6 my ( $spec, $base, $op ) = @_;
60 3         7 local $Data::Dumper::Terse = 1;
61 3         8 local $Data::Dumper::Indent = 0;
62 3         5 local $Data::Dumper::Sortkeys = 1;
63 3         7 local $Data::Dumper::Quotekeys = 0;
64 3         10 my $rm = _response_model_pkg( $spec, $base, $op );
65             my $h = {
66             operation_id => $op->{operation_id},
67             method => $op->{method},
68             path_template => $op->{path},
69             path_params => $op->{path_params},
70             query_params => $op->{query_params},
71             header_params => $op->{header_params},
72             has_body => $op->{has_body} ? 1 : 0,
73             response_model => $rm,
74 3 50       34 response_is_array => $op->{response_is_array} ? 1 : 0,
    50          
75             };
76 3         13 my $d = Data::Dumper::Dumper($h);
77 3         306 $d =~ s/^\$VAR1 = //;
78 3         8 $d =~ s/;\s*\z//s;
79 3         12 return $d;
80             }
81              
82             sub _core_pm {
83 3     3   19 my ( $pkg, $base, $sigs ) = @_;
84 3         7 my $hmac_use = grep { $_ eq 'hmac' } @$sigs;
  1         3  
85 3         6 my $bearer_use = grep { $_ eq 'bearer' } @$sigs;
  1         3  
86              
87 3         6 my $extra_use = '';
88 3         6 my $auth_default = '[]';
89 3 100       11 if ($hmac_use) {
    50          
90 1         3 $extra_use .= "use ${base}::Auth::Plugin::Hmac;\n";
91 1         2 $auth_default = "[ ${base}::Auth::Plugin::Hmac->new ]";
92             }
93             elsif ($bearer_use) {
94 0         0 $extra_use .= "use ${base}::Auth::Plugin::Bearer;\n";
95 0         0 $auth_default = "[ ${base}::Auth::Plugin::Bearer->new ]";
96             }
97              
98 3         38 my $rc = "${base}::Client::Result";
99 3         39 my $mid = <<"MID";
100             sub _build_openapi {
101             my (\$self) = \@_;
102             my \$file = \$self->openapi_schema_file
103             // croak 'openapi_schema_file required to validate requests and responses';
104             require YAML::PP;
105             my \$data = YAML::PP->new( boolean => 'JSON::PP' )->load_file(\$file);
106             return OpenAPI::Modern->new(
107             openapi_uri => '/',
108             openapi_schema => \$data,
109             );
110             }
111              
112             sub _request_validation_error {
113             my ( \$self, \$meta, \$tx ) = \@_;
114             return undef unless length( \$self->openapi_schema_file // '' );
115             my \$vr = \$self->openapi->validate_request(
116             \$tx->req,
117             {
118             path_template => \$meta->{path_template},
119             method => \$meta->{method},
120             operation_id => \$meta->{operation_id},
121             },
122             );
123             return undef if \$vr->valid;
124             return 'OpenAPI request validation failed: ' . "\$vr";
125             }
126              
127             sub _response_validation_error {
128             my ( \$self, \$meta, \$tx ) = \@_;
129             return undef unless length( \$self->openapi_schema_file // '' );
130             my \$vr = \$self->openapi->validate_response(
131             \$tx->res,
132             {
133             request => \$tx->req,
134             path_template => \$meta->{path_template},
135             method => \$meta->{method},
136             operation_id => \$meta->{operation_id},
137             },
138             );
139             return undef if \$vr->valid;
140             return 'OpenAPI response validation failed: ' . "\$vr";
141             }
142              
143             sub request_sync {
144             my ( \$self, \$meta, \$args ) = \@_;
145             \$self->ua->blocking(1);
146             my \$tx = \$self->build_tx( \$meta, \$args );
147             \$self->_apply_auth(\$tx, \$meta);
148             if ( my \$err = \$self->_request_validation_error( \$meta, \$tx ) ) {
149             croak \$err;
150             }
151             \$self->ua->start(\$tx);
152             return \$self->_result_from_tx( \$tx, \$meta );
153             }
154              
155             sub request_p {
156             my ( \$self, \$meta, \$args ) = \@_;
157             \$self->ua->blocking(0);
158             my \$tx = \$self->build_tx( \$meta, \$args );
159             \$self->_apply_auth(\$tx, \$meta );
160             if ( my \$err = \$self->_request_validation_error( \$meta, \$tx ) ) {
161             return Mojo::Promise->reject(\$err);
162             }
163             return \$self->ua->start_p(\$tx)->then(
164             sub (\$tx) {
165             return \$self->_result_from_tx( \$tx, \$meta );
166             }
167             );
168             }
169              
170             sub _result_from_tx {
171             my ( \$self, \$tx, \$meta ) = \@_;
172             if ( my \$err = \$self->_response_validation_error( \$meta, \$tx ) ) {
173             croak \$err;
174             }
175             my \$data = \$self->_inflate_response( \$tx, \$meta );
176             return ${rc}->new( tx => \$tx, data => \$data );
177             }
178              
179             sub _inflate_response {
180             my ( \$self, \$tx, \$meta ) = \@_;
181             my \$code = \$tx->res->code // 0;
182             return undef unless \$code >= 200 && \$code < 300;
183             my \$ct = \$tx->res->headers->content_type // '';
184             return undef unless \$ct =~ m{json}i;
185             my \$json = \$tx->res->json;
186             return undef unless defined \$json;
187             my \$pkg = \$meta->{response_model} // '';
188             return \$json if !length \$pkg;
189             my \$ok = eval { require \$pkg; 1 };
190             return \$json if !\$ok || !\$pkg->can('from_json');
191             my \$out = eval { \$pkg->from_json( \$json, \$meta->{response_is_array} // 0 ) };
192             return defined \$out ? \$out : \$json;
193             }
194              
195             MID
196              
197 3         37 return <<"HEAD" . $mid . <<'CORE';
198             package $pkg;
199              
200             use v5.26;
201             use Modern::Perl::Prelude -class;
202              
203             use Moo;
204             use Types::Standard qw(Str ArrayRef InstanceOf);
205             use OpenAPI::Modern;
206             use Mojo::UserAgent;
207             use Mojo::URL;
208             use Mojo::JSON qw(encode_json);
209             use Mojo::Promise;
210             use Carp qw(croak);
211             use ${base}::Client::Result;
212             $extra_use
213              
214             has base_url => (
215             is => 'ro',
216             isa => Str,
217             required => 1,
218             );
219              
220             has openapi_schema_file => (
221             is => 'ro',
222             isa => Str,
223             );
224              
225             has openapi => (
226             is => 'lazy',
227             isa => InstanceOf ['OpenAPI::Modern'],
228             init_arg => undef,
229             );
230              
231             has ua => (
232             is => 'lazy',
233             isa => InstanceOf ['Mojo::UserAgent'],
234             default => sub { Mojo::UserAgent->new },
235             );
236              
237             has auth_plugins => (
238             is => 'ro',
239             isa => ArrayRef,
240             default => sub { $auth_default },
241             );
242              
243             HEAD
244              
245             sub build_tx {
246             my ( $self, $meta, $args ) = @_;
247             $args //= {};
248              
249             my $path = $meta->{path_template};
250             for my $name ( @{ $meta->{path_params} // [] } ) {
251             croak "missing path param '$name'" unless exists $args->{$name};
252             my $v = $args->{$name};
253             $path =~ s/\{\Q$name\E\}/$v/g;
254             }
255              
256             my $url = Mojo::URL->new( $self->base_url . $path );
257             my %q;
258             for my $q ( @{ $meta->{query_params} // [] } ) {
259             $q{$q} = $args->{$q} if exists $args->{$q};
260             }
261             $url->query( \%q ) if keys %q;
262              
263             my $headers = { Accept => 'application/json' };
264             for my $h ( @{ $meta->{header_params} // [] } ) {
265             $headers->{$h} = $args->{$h} if exists $args->{$h};
266             }
267              
268             my $body;
269             if ( $meta->{has_body} ) {
270             my $payload = $args->{body} // croak 'body required';
271             $headers->{'Content-Type'} //= 'application/json';
272             $body = ref($payload) ? encode_json($payload) : $payload;
273             }
274              
275             my $method = $meta->{method};
276             return $self->ua->build_tx( $method => $url => $headers => $body );
277             }
278              
279             sub _apply_auth {
280             my ( $self, $tx, $meta ) = @_;
281             for my $p ( @{ $self->auth_plugins } ) {
282             $p->apply( $tx, $meta );
283             }
284             }
285              
286             1;
287             CORE
288             }
289              
290             sub _ops_pm {
291 3     3   13 my ( $pkg, $core_pkg, $ops, $sync, $async, $spec, $base ) = @_;
292 3         6 my @methods;
293 3         9 for my $op (@$ops) {
294 3         12 my $sub = _sanitize_sub( $op->{operation_id} );
295 3         12 my $meta = _meta_literal( $spec, $base, $op );
296 3         12 push @methods, <<"SUB";
297             sub $sub {
298             my ( \$self, \%args ) = \@_;
299             my \$meta = $meta;
300             return \$self->core->request_sync( \$meta, \\\%args ) if \$self->sync_mode;
301             return \$self->core->request_p( \$meta, \\\%args );
302             }
303             SUB
304             }
305              
306 3         10 my $body = join "\n", @methods;
307 3         18 return <<"PM";
308             package $pkg;
309              
310             use v5.26;
311             use Modern::Perl::Prelude -class;
312              
313             use Moo::Role;
314             use Types::Standard qw(Bool InstanceOf);
315              
316             requires 'core';
317              
318             has sync_mode => (
319             is => 'ro',
320             isa => Bool,
321             required => 1,
322             );
323              
324             $body
325              
326             1;
327             PM
328             }
329              
330             sub _sync_pm {
331 3     3   9 my ( $pkg, $ops_pkg, $core_pkg ) = @_;
332 3         16 return <<"PM";
333             package $pkg;
334              
335             use v5.26;
336             use Modern::Perl::Prelude -class;
337              
338             use Moo;
339             use Types::Standard qw(InstanceOf);
340              
341             has core => (
342             is => 'ro',
343             isa => InstanceOf ['$core_pkg'],
344             required => 1,
345             );
346              
347             with '$ops_pkg';
348              
349             sub sync_mode { 1 }
350              
351             1;
352             PM
353             }
354              
355             sub _async_pm {
356 3     3   41 my ( $pkg, $ops_pkg, $core_pkg ) = @_;
357 3         16 return <<"PM";
358             package $pkg;
359              
360             use v5.26;
361             use Modern::Perl::Prelude -class;
362              
363             use Moo;
364             use Types::Standard qw(InstanceOf);
365              
366             has core => (
367             is => 'ro',
368             isa => InstanceOf ['$core_pkg'],
369             required => 1,
370             );
371              
372             with '$ops_pkg';
373              
374             sub sync_mode { 0 }
375              
376             1;
377             PM
378             }
379              
380             1;
381              
382             __END__