File Coverage

blib/lib/Modern/OpenAPI/Generator/CodeGen/ClientModels.pm
Criterion Covered Total %
statement 145 162 89.5
branch 43 68 63.2
condition 37 76 48.6
subroutine 18 18 100.0
pod 1 1 100.0
total 244 325 75.0


line stmt bran cond sub pod time code
1             package Modern::OpenAPI::Generator::CodeGen::ClientModels;
2              
3 6     6   57 use v5.26;
  6         17  
4 6     6   22 use strict;
  6         7  
  6         96  
5 6     6   19 use warnings;
  6         41  
  6         283  
6 6     6   23 use Carp qw(croak);
  6         9  
  6         294  
7 6     6   25 use Modern::OpenAPI::Generator ();
  6         7  
  6         12278  
8              
9             sub _pathify_dir {
10 4     4   8 my ($name) = @_;
11 4         19 $name =~ s{::}{/}g;
12 4         10 return $name;
13             }
14              
15             sub generate {
16 4     4 1 17 my ( $class, %arg ) = @_;
17 4   33     16 my $writer = $arg{writer} // croak 'writer';
18 4   33     44 my $spec = $arg{spec} // croak 'spec';
19 4   33     46 my $base = $arg{base} // croak 'base';
20              
21 4         14 my $lib = 'lib/' . _pathify_dir($base);
22 4         8 my $result_pkg = "$base\::Client::Result";
23 4         17 $writer->write( "$lib/Client/Result.pm", _result_pm( $result_pkg, $spec ) );
24              
25 4   50     20 my $schemas = $spec->raw->{components}{schemas} // {};
26 4 50 33     49 return unless ref $schemas eq 'HASH' && %$schemas;
27              
28 4         10 my $mbase_pkg = "$base\::Model::Base";
29 4         19 $writer->write( "$lib/Model/Base.pm", _model_base_pm( $mbase_pkg, $spec ) );
30              
31 4         21 for my $name ( sort keys %$schemas ) {
32 5         15 my $sch = $schemas->{$name};
33 5 50       19 next unless ref $sch eq 'HASH';
34 5 50       36 next if _schema_skipped($sch);
35 5         27 ( my $file_safe = $name ) =~ s/[^A-Za-z0-9._-]/_/g;
36 5         12 my $pkg = _schema_name_to_pkg( $base, $name );
37 5         19 my $pm = _one_model_pm( $spec, $base, $mbase_pkg, $name, $pkg, $sch, $schemas );
38 5 50       27 $writer->write( "$lib/Model/$file_safe.pm", $pm ) if length $pm;
39             }
40             }
41              
42             sub _schema_skipped {
43 6     6   15 my ($sch) = @_;
44 6 50 33     54 return 1 if $sch->{allOf} || $sch->{oneOf} || $sch->{anyOf};
      33        
45 6 50 50     62 return 0 if ( $sch->{type} // '' ) eq 'object' && ref $sch->{properties} eq 'HASH';
      33        
46 0         0 return 1;
47             }
48              
49             sub _schema_name_to_pkg {
50 6     6   15 my ( $base, $name ) = @_;
51 6         16 ( my $safe = $name ) =~ s/[^A-Za-z0-9_]/_/g;
52 6         14 return "$base\::Model::$safe";
53             }
54              
55             sub _ref_name {
56 11     11   51 my ($ref) = @_;
57 11 100 66     52 return undef unless defined $ref && $ref =~ m{\#/components/schemas/([^/]+)\z};
58 1         4 return $1;
59             }
60              
61             sub _esc {
62 20     20   29 my ($s) = @_;
63 20         37 $s =~ s/'/\\'/g;
64 20         41 return $s;
65             }
66              
67             sub _wrap_req {
68 10     10   20 my ( $required, $inner ) = @_;
69 10 100       18 return $inner if $required;
70 6         27 return "Maybe[$inner]";
71             }
72              
73             # OpenAPI Generator-style POD: API info in =begin comment (safe for long markdown).
74             sub _api_pod_preamble {
75 18     18   51 my ( $spec, %opt ) = @_;
76 18   50     47 my $info = $spec->raw->{info} // {};
77 18   50     41 my $title = $info->{title} // 'API';
78 18   50     54 my $desc = $info->{description} // '';
79 18         22 $desc =~ s/\r\n/\n/g;
80 18         22 $desc =~ s/^=end\s+comment\s*$/[line removed: looked like =end comment]/gim;
81 18   50     34 my $ver = $info->{version} // '';
82 18         40 my $oav = $spec->openapi_version;
83 18         24 my $gen = 'Modern::OpenAPI::Generator';
84 18         22 my $genurl = 'https://metacpan.org/pod/Modern::OpenAPI::Generator';
85 18   50     30 my $gver = $Modern::OpenAPI::Generator::VERSION // '0';
86              
87 18         21 my $extra = '';
88 18 100       38 if ( my $p = $opt{purpose} ) {
89 8         15 $extra .= "\n$p\n";
90             }
91 18 100       40 if ( my $sn = $opt{schema_name} ) {
92 10         17 $extra .= "\nThis module represents OpenAPI C.\n";
93             }
94 18 50       39 if ( my $sd = $opt{schema_description} ) {
95 0         0 $sd =~ s/\r\n/\n/g;
96 0         0 $sd =~ s/^=end\s+comment\s*$/[line removed]/gim;
97 0         0 $extra .= "\n---\n\nSchema description:\n\n$sd\n";
98             }
99              
100 18         74 return <<"POD";
101              
102             =begin comment
103              
104             $title
105              
106             $desc
107             $extra
108              
109             The version of the OpenAPI document: $oav
110             API version (info.version): $ver
111              
112             Perl generator ($gen) version: $gver
113             Generated by: $gen ($genurl)
114              
115             =end comment
116              
117             =cut
118              
119             #
120             # NOTE: This file is auto generated by $gen.
121             # Do not edit the class manually.
122             # Ref: $genurl
123             #
124              
125             POD
126             }
127              
128             sub _result_pm {
129 4     4   11 my ( $pkg, $spec ) = @_;
130 4         14 my $pod = _api_pod_preamble(
131             $spec,
132             purpose =>
133             'Client result object: holds L (C) and response C (JSON or C<::Model::*>).',
134             );
135 4         43 return <<"PM";
136             $pod
137             package $pkg;
138              
139             use v5.26;
140             use strict;
141             use warnings;
142              
143             use Moo;
144              
145             has tx => (
146             is => 'ro',
147             required => 1,
148             );
149              
150             has data => (
151             is => 'ro',
152             );
153              
154             1;
155             PM
156             }
157              
158             sub _model_base_pm {
159 4     4   12 my ( $pkg, $spec ) = @_;
160 4         10 my $pod = _api_pod_preamble(
161             $spec,
162             purpose => 'Moo::Role included by all generated C<::Model::*> classes.',
163             );
164 4         26 return <<"PM";
165             $pod
166             package $pkg;
167              
168             use v5.26;
169             use strict;
170             use warnings;
171              
172             use Moo::Role;
173              
174             1;
175             PM
176             }
177              
178             # Expression to read attribute $pname from $self in generated model (handles odd keys).
179             sub _perl_self_accessor {
180 10     10   15 my ($pname) = @_;
181 10 50       50 if ( $pname =~ /^[a-zA-Z_]\w*$/a ) {
182 10         44 return "\$self->$pname";
183             }
184 0         0 ( my $q = $pname ) =~ s/'/\\'/g;
185 0         0 return "\$self->\${\\'$q\\'}";
186             }
187              
188             sub _one_model_pm {
189 5     5   15 my ( $spec, $base, $mbase, $schema_name, $pkg, $sch, $all_schemas ) = @_;
190              
191 5   50     16 my $props = $sch->{properties} // {};
192 5 50 33     37 return '' unless ref $props eq 'HASH' && %$props;
193              
194 5   100     45 my $req = $sch->{required} // [];
195 5         13 my %req = map { $_ => 1 } @$req;
  4         29  
196              
197 5         12 my @has;
198             my @buildargs;
199              
200 5         20 for my $pname ( sort keys %$props ) {
201 10         16 my $ps = $props->{$pname};
202 10 50       21 next unless ref $ps eq 'HASH';
203             my ( $has_line, $bld ) =
204             _prop_type( $base, $schema_name, $pkg, $pname, $ps, $all_schemas,
205 10         31 $req{$pname} );
206 10         35 push @has, $has_line;
207 10 100       25 push @buildargs, $bld if length $bld;
208             }
209              
210 5         6 my @to_json_lines;
211 5         17 for my $pname ( sort keys %$props ) {
212 10         15 my $ps = $props->{$pname};
213 10 50       20 next unless ref $ps eq 'HASH';
214 10         15 push @to_json_lines,
215             sprintf q{ '%s' => _to_json_val( %s ),}, _esc($pname),
216             _perl_self_accessor($pname);
217             }
218              
219 5         8 my $to_json_block = '';
220 5 50       11 if (@to_json_lines) {
221 5         44 $to_json_block = join "\n",
222             'sub TO_JSON {',
223             ' my $self = shift;',
224             ' return {',
225             join( "\n", @to_json_lines ),
226             ' };',
227             '}',
228             '',
229             'sub _to_json_val {',
230             ' my ($v) = @_;',
231             ' return undef unless defined $v;',
232             q{ if ( ref $v eq 'ARRAY' ) { return [ map { _to_json_val($_) } @$v ]; }},
233             q{ if ( blessed($v) && $v->can('TO_JSON') ) { return $v->TO_JSON; }},
234             ' return $v;',
235             '}',
236             '',
237             'sub to_json {',
238             ' my $self = shift;',
239             ' require Mojo::JSON;',
240             ' return Mojo::JSON::encode_json( $self->TO_JSON );',
241             '}',
242             '';
243             }
244              
245 5         10 my $buildargs_block = '';
246 5 100       15 if (@buildargs) {
247 1         3 $buildargs_block = join "\n",
248             'around BUILDARGS => sub {',
249             ' my ( $orig, $class, $args ) = @_;',
250             ' $args = {} unless ref $args eq \'HASH\';',
251             @buildargs,
252             ' return $class->$orig($args);',
253             '};',
254             '';
255             }
256              
257 5         19 my $uses = join "\n",
258             'use v5.26;',
259             'use strict;',
260             'use warnings;',
261             "use Moo;",
262             "with '$mbase';",
263             'use Types::Standard qw( Str Num Int Bool ArrayRef HashRef Maybe Enum );',
264             'use Scalar::Util qw(blessed);',
265             '';
266              
267 5         12 my $has_block = join "\n", @has, '';
268              
269             my $pod_top = _api_pod_preamble(
270             $spec,
271             schema_name => $schema_name,
272 5   50     29 schema_description => ( $sch->{description} // '' ),
273             );
274             my $pod_end = _api_pod_preamble(
275             $spec,
276             schema_name => $schema_name,
277 5   50     20 schema_description => ( $sch->{description} // '' ),
278             );
279              
280 5         53 return <<"PM";
281             $pod_top
282             package $pkg;
283              
284             $uses
285             $has_block
286             $buildargs_block
287             sub from_json {
288             my ( \$class, \$data, \$is_array ) = \@_;
289             \$is_array //= 0;
290             if (\$is_array) {
291             return undef unless defined \$data && ref \$data eq 'ARRAY';
292             return [ map { __PACKAGE__->_from_hash(\$_) } \@\$data ];
293             }
294             return \$class->_from_hash(\$data);
295             }
296              
297             sub _from_hash {
298             my ( \$class, \$data ) = \@_;
299             return undef unless defined \$data && ref \$data eq 'HASH';
300             return \$class->new(\%\$data);
301             }
302              
303             $to_json_block
304             $pod_end
305             1;
306             PM
307             }
308              
309             sub _prop_type {
310 10     10   31 my ( $base, $schema_name, $pkg, $pname, $ps, $all_schemas, $required ) = @_;
311              
312 10         22 my $qe = _esc($pname);
313              
314 10 100 100     45 if ( my $r = _ref_name( $ps->{'$ref'} // '' ) ) {
315 1         3 my $npkg = _schema_name_to_pkg( $base, $r );
316             my $inner =
317 1 50 33     18 ( !$all_schemas->{$r} || _schema_skipped( $all_schemas->{$r} ) )
    50          
318             ? 'HashRef'
319             : ( $r eq $schema_name ? 'HashRef' : "InstanceOf['$npkg']" );
320 1         2 my $type = _wrap_req( $required, $inner );
321 1         3 my $has = "has '$qe' => ( is => 'ro', isa => $type );";
322 1 50       2 return ( $has, '' ) if $inner eq 'HashRef';
323              
324 1         3 my $bld = <
325             if ( exists \$args->{'$qe'} && ref \$args->{'$qe'} eq 'HASH' ) {
326             \$args->{'$qe'} = ${npkg}->_from_hash( \$args->{'$qe'} );
327             }
328             BUILD
329 1         3 return ( $has, $bld );
330             }
331              
332 9 100 50     41 if ( ( $ps->{type} // '' ) eq 'array' && ref $ps->{items} eq 'HASH' ) {
      66        
333 1         3 my $it = $ps->{items};
334 1 50 50     4 if ( my $r = _ref_name( $it->{'$ref'} // '' ) ) {
335 0         0 my $npkg = _schema_name_to_pkg( $base, $r );
336             my $inner =
337 0 0 0     0 ( !$all_schemas->{$r} || _schema_skipped( $all_schemas->{$r} ) )
    0          
338             ? 'ArrayRef[HashRef]'
339             : (
340             $r eq $schema_name
341             ? 'ArrayRef[HashRef]'
342             : "ArrayRef[InstanceOf['$npkg']]"
343             );
344 0         0 my $type = _wrap_req( $required, $inner );
345 0         0 my $has = "has '$qe' => ( is => 'ro', isa => $type );";
346 0 0       0 return ( $has, '' ) if $inner eq 'ArrayRef[HashRef]';
347              
348 0         0 my $bld = <
349             if ( exists \$args->{'$qe'} && ref \$args->{'$qe'} eq 'ARRAY' ) {
350             \$args->{'$qe'} = [ map { ref \$_ eq 'HASH' ? ${npkg}->_from_hash(\$_) : \$_ } \@{\$args->{'$qe'}} ];
351             }
352             BUILD
353 0         0 return ( $has, $bld );
354             }
355 1         2 my $inner = 'ArrayRef[Str]';
356 1 50 50     18 if ( ( $it->{type} // '' ) eq 'number' ) {
    50 50        
357 0         0 $inner = 'ArrayRef[Num]';
358             }
359             elsif ( ( $it->{type} // '' ) eq 'integer' ) {
360 0         0 $inner = 'ArrayRef[Int]';
361             }
362 1         2 my $type = _wrap_req( $required, $inner );
363 1         3 return ( "has '$qe' => ( is => 'ro', isa => $type );", '' );
364             }
365              
366 8   50     17 my $t = $ps->{type} // 'string';
367 8 100 100     24 if ( $t eq 'string' && ref $ps->{enum} eq 'ARRAY' && @{ $ps->{enum} } ) {
  1   66     4  
368 1         1 my @q = map { qq{'$_'} } @{ $ps->{enum} };
  2         5  
  1         3  
369 1         4 my $enum = 'Enum[' . join( ',', @q ) . ']';
370 1         2 my $type = _wrap_req( $required, $enum );
371 1         3 return ( "has '$qe' => ( is => 'ro', isa => $type );", '' );
372             }
373 7 100       17 if ( $t eq 'integer' ) {
374 1         3 my $type = _wrap_req( $required, 'Int' );
375 1         4 return ( "has '$qe' => ( is => 'ro', isa => $type );", '' );
376             }
377 6 100       14 if ( $t eq 'number' ) {
378 1         2 my $type = _wrap_req( $required, 'Num' );
379 1         3 return ( "has '$qe' => ( is => 'ro', isa => $type );", '' );
380             }
381 5 100       14 if ( $t eq 'boolean' ) {
382 4         11 my $type = _wrap_req( $required, 'Bool' );
383             return (
384 4         16 "has '$qe' => ( is => 'ro', isa => $type, coerce => sub { my \$v = shift; \$v ? 1 : 0 } );",
385             ''
386             );
387             }
388 1 50       3 if ( $t eq 'object' ) {
389 0         0 my $type = _wrap_req( $required, 'HashRef' );
390 0         0 return ( "has '$qe' => ( is => 'ro', isa => $type );", '' );
391             }
392              
393 1         7 my $type = _wrap_req( $required, 'Str' );
394 1         4 return ( "has '$qe' => ( is => 'ro', isa => $type );", '' );
395             }
396              
397             1;
398              
399             __END__