File Coverage

blib/lib/Modern/OpenAPI/Generator/CodeGen/Docs.pm
Criterion Covered Total %
statement 263 344 76.4
branch 66 160 41.2
condition 55 158 34.8
subroutine 26 27 96.3
pod 1 1 100.0
total 411 690 59.5


line stmt bran cond sub pod time code
1             package Modern::OpenAPI::Generator::CodeGen::Docs;
2              
3 6     6   56 use v5.26;
  6         16  
4 6     6   22 use strict;
  6         8  
  6         121  
5 6     6   17 use warnings;
  6         9  
  6         205  
6 6     6   2184 use utf8;
  6         1251  
  6         24  
7 6     6   184 use Carp qw(croak);
  6         7  
  6         275  
8 6     6   27 use Modern::OpenAPI::Generator ();
  6         8  
  6         25519  
9              
10             sub _perl_method {
11 13     13   33 my ($operation_id) = @_;
12 13 50 33     64 return 'operation' unless defined $operation_id && length $operation_id;
13 13         47 ( my $s = $operation_id ) =~ s{([a-z])([A-Z])}{$1_$2}g;
14 13         56 $s =~ s{[^A-Za-z0-9]+}{_}g;
15 13         35 return lc $s;
16             }
17              
18             # First OpenAPI tag -> OpenAPI-Generator-style name (e.g. "Recurring Payments" -> RecurringPaymentsApi).
19             sub _tag_to_api_class {
20 7     7   14 my ($tags) = @_;
21 7 50 33     34 my $t = ( ref $tags eq 'ARRAY' && @$tags ) ? $tags->[0] : 'Default';
22 7         36 $t =~ s/^\s+|\s+$//g;
23 7 50       15 return 'DefaultApi' unless length $t;
24 7         19 my @w = grep { length } split /\s+/, $t;
  7         20  
25 7         15 my $pascal = join '', map { ucfirst lc $_ } @w;
  7         31  
26 7         14 $pascal =~ s/[^A-Za-z0-9]//g;
27 7 50       26 return 'DefaultApi' unless length $pascal;
28 7         20 return $pascal . 'Api';
29             }
30              
31             sub _first_tag_label {
32 3     3   4 my ($tags) = @_;
33 3 50 33     17 return 'Default' unless ref $tags eq 'ARRAY' && @$tags;
34 0         0 my $t = $tags->[0];
35 0         0 $t =~ s/^\s+|\s+$//g;
36 0 0       0 return length $t ? $t : 'Default';
37             }
38              
39             sub _md_cell {
40 29     29   42 my ($s) = @_;
41 29 50       47 return '' unless defined $s;
42 29         42 $s =~ s/\r?\n/ /g;
43 29         37 $s =~ s/\|/\\\|/g;
44 29         150 return $s;
45             }
46              
47             sub _ref_to_schema_name {
48 1     1   14 my ($ref) = @_;
49 1 50 33     6 return '' unless defined $ref && length $ref;
50 1 50       12 return $1 if $ref =~ m{\#/components/schemas/([^/]+)\z};
51 0 0       0 return $1 if $ref =~ m{/([^/]+)\z};
52 0         0 return $ref;
53             }
54              
55             # Same rule as CodeGen::ClientModels: Perl package for components/schemas name.
56             sub _model_pkg {
57 10     10   21 my ( $base, $schema_name ) = @_;
58 10         29 ( my $safe = $schema_name ) =~ s/[^A-Za-z0-9_]/_/g;
59 10         27 return "$base\::Model::$safe";
60             }
61              
62             sub _schema_type_line {
63 11     11   18 my ( $sch, $for_doc_link ) = @_;
64 11   50     26 $for_doc_link //= 0;
65 11 50       21 return '' unless ref $sch eq 'HASH';
66 11   100     28 my $ref = $sch->{'$ref'} // '';
67 11 100       65 if ( length $ref ) {
68 1         3 my $n = _ref_to_schema_name($ref);
69 1 50 33     7 return $for_doc_link && length $n ? "[$n]($n.md)" : $n;
70             }
71 10   50     23 my $t = $sch->{type} // 'any';
72 10 100 66     29 if ( $t eq 'array' && ref $sch->{items} eq 'HASH' ) {
73 1         5 return 'array[' . _schema_type_line( $sch->{items}, $for_doc_link ) . ']';
74             }
75 9 50 33     34 if ( $t eq 'object' && ref $sch->{properties} eq 'HASH' ) {
76 0         0 return 'object';
77             }
78 9 50       55 $t .= ' / ' . $sch->{format} if $sch->{format};
79 9         67 return $t;
80             }
81              
82             sub _merged_parameters_list {
83 6     6   10 my ( $spec, $op ) = @_;
84 6         10 my $path = $op->{path};
85 6   50     13 my $item = $spec->raw->{paths}{$path} // {};
86 6         10 my @p;
87 6 50       15 push @p, @{ $item->{parameters} } if ref $item->{parameters} eq 'ARRAY';
  0         0  
88 6         15 my $m = lc $op->{method};
89 6   50     13 my $opobj = $item->{$m} // {};
90 6 50       17 push @p, @{ $opobj->{parameters} } if ref $opobj->{parameters} eq 'ARRAY';
  0         0  
91 6         17 return \@p;
92             }
93              
94             sub _security_summary_md {
95 3     3   21 my ( $spec, $op ) = @_;
96 3   50     5 my @defs = @{ $spec->raw->{security} // [] };
  3         8  
97 3   50     4 my @opsec = @{ $op->{operation_hash}{security} // [] };
  3         16  
98 3 50       10 my @sec = @opsec ? @opsec : @defs;
99 3   50     7 my $schemes = $spec->raw->{components}{securitySchemes} // {};
100 3 50       18 return 'None (or inherited from global `security` in the OpenAPI document).' unless @sec;
101              
102 0         0 my @names;
103 0         0 for my $h (@sec) {
104 0 0       0 next unless ref $h eq 'HASH';
105 0         0 push @names, keys %$h;
106             }
107 0         0 @names = sort keys %{ { map { $_ => 1 } @names } };
  0         0  
  0         0  
108 0         0 return join ', ', map { '`' . _md_cell($_) . '`' } @names;
  0         0  
109             }
110              
111             sub _resolve_request_body_schema {
112 0     0   0 my ($op) = @_;
113 0   0     0 my $rb = $op->{operation_hash}{requestBody} // return undef;
114 0 0       0 return undef unless ref $rb eq 'HASH';
115 0   0     0 my $c = $rb->{content} // {};
116 0   0     0 my $json = $c->{'application/json'} // $c->{'application/*+json'};
117 0 0       0 return undef unless ref $json eq 'HASH';
118 0         0 my $sch = $json->{schema};
119 0 0       0 return ref $sch eq 'HASH' ? $sch : undef;
120             }
121              
122             sub _documentation_for_api_endpoints {
123 4     4   9 my ( $spec, $ops, $client ) = @_;
124 4         15 my $raw = $spec->raw;
125 4         7 my $base_url = '';
126 4 50 33     17 if ( ref $raw->{servers} eq 'ARRAY' && @{ $raw->{servers} } ) {
  0         0  
127 0   0     0 $base_url = $raw->{servers}[0]{url} // '';
128             }
129              
130 4         10 my @lines = ( '## DOCUMENTATION FOR API ENDPOINTS', '' );
131 4 50       10 if ( length $base_url ) {
132 0         0 push @lines, "All URIs are relative to *$base_url*", '';
133             }
134              
135 4 100       9 if ($client) {
136 3         13 push @lines,
137             '| Class | Method | HTTP request | Description |',
138             '|-------|--------|--------------|-------------|';
139 3         7 for my $op (@$ops) {
140 3         9 my $class = _tag_to_api_class( $op->{tags} );
141 3         6 my $file = "$class.md";
142 3         6 my $meth = _perl_method( $op->{operation_id} );
143 3   50     25 my $sum = _md_cell( $op->{operation_hash}{summary} // '' );
144 3         31 ( my $path = $op->{path} ) =~ s/\|/\\\|/g;
145             push @lines,
146             sprintf
147             '| *[%s](docs/%s)* | [**%s**](docs/%s#%s) | **%s** `%s` | %s |',
148 3         13 $class, $file, $meth, $file, $meth, $op->{method}, $path, $sum;
149             }
150             }
151             else {
152 1         2 push @lines,
153             '| Class | Method | HTTP request |',
154             '|-------|--------|--------------|';
155 1         2 for my $op (@$ops) {
156 1         4 my $class = _tag_to_api_class( $op->{tags} );
157 1         5 my $meth = _perl_method( $op->{operation_id} );
158 1         6 ( my $path = $op->{path} ) =~ s/\|/\\\|/g;
159             push @lines,
160             sprintf '| *%s* | `%s` | **%s** `%s` |',
161 1         5 $class, $meth, $op->{method}, $path;
162             }
163             }
164              
165 4 50       18 if ( !@$ops ) {
166 0 0       0 push @lines, '| — | — | — | — |' if $client;
167 0 0       0 push @lines, '| — | — | — |' if !$client;
168             }
169              
170 4         21 return join( "\n", @lines ) . "\n\n";
171             }
172              
173             sub _documentation_for_models {
174 4     4   9 my ( $spec, $base ) = @_;
175 4   50     14 my $schemas = $spec->raw->{components}{schemas} // {};
176 4 50 33     44 return '' unless ref $schemas eq 'HASH' && %$schemas;
177              
178 4         11 my @lines = ( '## DOCUMENTATION FOR MODELS', '' );
179 4         18 for my $name ( sort keys %$schemas ) {
180 5         17 my $pkg = _model_pkg( $base, $name );
181 5         23 push @lines, "- [`$pkg`](docs/$name.md)";
182             }
183 4         18 return join "\n", @lines, '';
184             }
185              
186             sub _tag_api_operation_md {
187 3     3   12 my ( $spec, $base, $class, $op ) = @_;
188 3         8 my $meth = _perl_method( $op->{operation_id} );
189 3         6 my $oh = $op->{operation_hash};
190 3   50     10 my $sum = $oh->{summary} // '';
191 3   50     12 my $des = $oh->{description} // '';
192 3         11 ( my $path_e = $op->{path} ) =~ s/\|/\\\|/g;
193              
194 3         4 my @out;
195 3         18 push @out, "", '', "## `$meth`", '';
196 3         8 push @out, '**operationId:** `' . _md_cell( $op->{operation_id} ) . '` ', '';
197 3         10 push @out, '**HTTP:** **' . $op->{method} . '** `' . $path_e . '`', '';
198              
199 3 50       9 if ( length $sum ) {
200 3         22 push @out, '### Summary', '', $sum, '';
201             }
202 3 50       9 if ( length $des ) {
203 0         0 push @out, '### Description', '', $des, '';
204             }
205              
206 3         11 my $params = _merged_parameters_list( $spec, $op );
207 3 50       9 if ( @$params ) {
208 0         0 push @out, '### Parameters', '',
209             '| Name | In | Type | Required | Description |',
210             '|------|----|------|----------|-------------|';
211 0         0 for my $p (@$params) {
212 0   0     0 my $sch = $p->{schema} // {};
213 0 0       0 my $typ = _schema_type_line( ref $sch eq 'HASH' ? $sch : {}, 1 );
214             push @out,
215             sprintf '| `%s` | %s | %s | %s | %s |',
216             _md_cell( $p->{name} // '' ),
217             _md_cell( $p->{in} // '' ),
218             $typ,
219             ( $p->{required} ? 'yes' : 'no' ),
220 0 0 0     0 _md_cell( $p->{description} // '' );
      0        
      0        
221             }
222 0         0 push @out, '';
223             }
224              
225 3 50       29 if ( $op->{has_body} ) {
226 0         0 push @out, '### Request body', '';
227 0         0 my $rb = $oh->{requestBody};
228 0 0 0     0 if ( ref $rb eq 'HASH' && $rb->{description} ) {
229 0         0 push @out, $rb->{description}, '';
230             }
231 0         0 my $rs = _resolve_request_body_schema($op);
232 0 0       0 if ( ref $rs eq 'HASH' ) {
233 0 0 0     0 if ( my $ref = $rs->{'$ref'} ) {
    0 0        
234 0         0 my $n = _ref_to_schema_name($ref);
235 0 0       0 push @out,
236             'JSON body must match schema '
237             . ( length $n ? "[$n]($n.md)." : '(see OpenAPI `requestBody`).' ),
238             '';
239             }
240             elsif ( ( $rs->{type} // '' ) eq 'object' && ref $rs->{properties} eq 'HASH' ) {
241 0         0 push @out,
242             '| Field | Type | Required | Description |',
243             '|-------|------|----------|-------------|';
244 0   0     0 my $req = $rs->{required} // [];
245 0         0 my %r = map { $_ => 1 } @$req;
  0         0  
246 0         0 for my $k ( sort keys %{ $rs->{properties} } ) {
  0         0  
247 0         0 my $ps = $rs->{properties}{$k};
248             push @out,
249             sprintf '| `%s` | %s | %s | %s |',
250             _md_cell($k),
251             _schema_type_line( ref $ps eq 'HASH' ? $ps : {}, 1 ),
252             ( $r{$k} ? 'yes' : 'no' ),
253             _md_cell(
254 0 0 0     0 ref $ps eq 'HASH' ? ( $ps->{description} // '' ) : ''
    0          
    0          
255             );
256             }
257 0         0 push @out, '';
258             }
259             else {
260 0         0 push @out, 'Type: `' . _schema_type_line($rs) . '`', '';
261             }
262             }
263             else {
264 0         0 push @out, '(See OpenAPI `requestBody` in `share/openapi.yaml`.)', '';
265             }
266             }
267              
268 3         11 push @out, '### Authorization', '', _security_summary_md( $spec, $op ), '', '### Client example', '',
269             '```perl', "my \$r = \$client->$meth(", _perl_example_args( $spec, $op ), ');',
270             'my \$data = \$r->data; # response object or plain JSON',
271             'my \$tx = \$r->tx; # Mojo::Transaction', '```', '',
272             '[[Back to API list]](../README.md#documentation-for-api-endpoints)', '';
273              
274 3         34 return join "\n", @out;
275             }
276              
277             sub _perl_example_args {
278 3     3   29 my ( $spec, $op ) = @_;
279 3         6 my @parts;
280 3         6 my $params = _merged_parameters_list( $spec, $op );
281 3         8 for my $p (@$params) {
282 0 0       0 next unless $p->{required};
283 0   0     0 my $n = $p->{name} // next;
284 0         0 push @parts, " $n => '...',";
285             }
286 3 50       11 if ( $op->{has_body} ) {
287 0         0 push @parts, ' body => { ... },';
288             }
289 3         67 return join "\n", @parts;
290             }
291              
292             sub _tag_api_file_md {
293 3     3   8 my ( $spec, $base, $class, $ops ) = @_;
294 3         11 my $tag = _first_tag_label( $ops->[0]{tags} );
295 3         6 my $sync = "$base\::Client::Sync";
296 3         5 my $async = "$base\::Client::Async";
297 3         6 my $core = "$base\::Client::Core";
298 3         9 my $raw = $spec->raw;
299 3         6 my $base_u = '';
300 3 50 33     12 if ( ref $raw->{servers} eq 'ARRAY' && @{ $raw->{servers} } ) {
  0         0  
301 0   0     0 $base_u = $raw->{servers}[0]{url} // '';
302             }
303              
304 3         4 my @top;
305 3         8 push @top, "# $class", '';
306 3         15 push @top,
307             "Operations tagged **$tag** in the OpenAPI document. "
308             . "Call these methods on [`$sync`](../README.md#client-usage) "
309             . "or [`$async`](../README.md#client-usage) "
310             . "(see also [`$core`](../README.md#client-usage)).",
311             '';
312 3         31 push @top, '```perl', "use $sync;", "use Path::Tiny qw(path);",
313             "my \$core = $core->new(",
314             " base_url => 'https://api.example.com',",
315             " openapi_schema_file => path('share/openapi.yaml')->absolute->stringify,",
316             ');',
317             "my \$client = $sync->new( core => \$core );",
318             '```', '';
319 3 50       8 if ( length $base_u ) {
320 0         0 push @top, "All URIs are relative to *$base_u*", '';
321             }
322              
323 3         6 push @top,
324             '| Method | HTTP request | Description |',
325             '|--------|--------------|-------------|';
326 3         20 for my $op (@$ops) {
327 3         12 my $meth = _perl_method( $op->{operation_id} );
328 3         12 ( my $p = $op->{path} ) =~ s/\|/\\\|/g;
329             push @top,
330             sprintf '| [**%s**](%s.md#%s) | **%s** `%s` | %s |',
331             $meth, $class, $meth, $op->{method}, $p,
332 3   50     17 _md_cell( $op->{operation_hash}{summary} // '' );
333             }
334 3         7 push @top, '';
335              
336 3         4 my @body;
337 3         6 for my $op (@$ops) {
338 3         13 push @body, _tag_api_operation_md( $spec, $base, $class, $op );
339 3         58 push @body, '---', '';
340             }
341              
342 3         30 return join "\n", @top, @body;
343             }
344              
345             sub _write_tag_api_docs {
346 3     3   7 my ( $writer, $spec, $base, $ops ) = @_;
347 3         6 my %by;
348 3         5 for my $op (@$ops) {
349 3         14 my $c = _tag_to_api_class( $op->{tags} );
350 3         6 push @{ $by{$c} }, $op;
  3         9  
351             }
352 3         9 for my $class ( sort keys %by ) {
353 3         13 my $md = _tag_api_file_md( $spec, $base, $class, $by{$class} );
354 3         14 $writer->write( "docs/$class.md", $md );
355             }
356             }
357              
358             sub _schema_properties_table {
359 5     5   11 my ( $spec, $name, $sch, $depth ) = @_;
360 5   50     62 $depth //= 0;
361 5 50       14 return '' if $depth > 3;
362 5 50       17 return '' unless ref $sch eq 'HASH';
363 5         11 my $ref = $sch->{'$ref'};
364 5 50       10 if ($ref) {
365 0         0 my $n = _ref_to_schema_name($ref);
366 0 0       0 return '' unless length $n;
367 0         0 return _schema_properties_table( $spec, $n, $spec->raw->{components}{schemas}{$n}, $depth + 1 );
368             }
369 5 50 50     19 return '' unless ( $sch->{type} // '' ) eq 'object';
370 5   50     15 my $props = $sch->{properties} // {};
371 5 50 33     24 return '' unless ref $props eq 'HASH' && %$props;
372 5   100     18 my $req = $sch->{required} // [];
373 5         12 my %r = map { $_ => 1 } @$req;
  4         104  
374              
375 5         16 my @lines = (
376             '| Name | Type | Description | Notes |',
377             '|------|------|-------------|-------|'
378             );
379 5         19 for my $k ( sort keys %$props ) {
380 10         17 my $ps = $props->{$k};
381             push @lines,
382             sprintf '| `%s` | %s | %s | %s |',
383             _md_cell($k),
384             _schema_type_line( ref $ps eq 'HASH' ? $ps : {}, 1 ),
385             _md_cell( ref $ps eq 'HASH' ? ( $ps->{description} // '' ) : '' ),
386 10 50 50     21 ( $r{$k} ? '[required]' : '[optional]' );
    50          
    100          
387             }
388 5         24 return join "\n", @lines;
389             }
390              
391             sub _model_doc_footer {
392 5     5   11 return join "\n",
393             '[[Back to Model list]](../README.md#documentation-for-models)',
394             '[[Back to API list]](../README.md#documentation-for-api-endpoints)',
395             '[[Back to README]](../README.md)';
396             }
397              
398             sub _schema_markdown {
399 5     5   14 my ( $spec, $base, $name, $sch ) = @_;
400 5         17 my $pkg = _model_pkg( $base, $name );
401 5         6 my @out;
402 5         14 push @out, "# $pkg", '';
403 5 50 50     60 if ( ref $sch eq 'HASH' && length( $sch->{description} // '' ) ) {
      33        
404 0         0 push @out, $sch->{description}, '';
405             }
406 5         58 push @out, '### Load the model package', '', '```perl', "use $pkg;", '```', '';
407 5 50       94 if ( ref $sch ne 'HASH' ) {
408 0         0 push @out, '', '(Non-object schema; see `share/openapi.yaml`.)', '';
409 0         0 push @out, '', _model_doc_footer();
410 0         0 return join "\n", @out;
411             }
412 5         10 my $ref = $sch->{'$ref'};
413 5 50       14 if ($ref) {
414 0         0 my $n = _ref_to_schema_name($ref);
415 0 0 0     0 if ( length $n && $n ne $name ) {
416 0         0 push @out, '', 'This schema is an alias of ', "[$n]($n.md).", '';
417             }
418 0         0 push @out, '', _model_doc_footer();
419 0         0 return join "\n", @out;
420             }
421 5         16 my $tbl = _schema_properties_table( $spec, $name, $sch, 0 );
422 5 50       13 if ( length $tbl ) {
423 5         14 push @out, '', '### Properties', '', $tbl, '';
424             }
425             else {
426 0         0 push @out, '',
427             'See `components.schemas.'
428             . $name
429             . '` in [`share/openapi.yaml`](../share/openapi.yaml).',
430             '';
431             }
432 5         16 push @out, '', _model_doc_footer();
433 5         25 return join "\n", @out;
434             }
435              
436             sub _write_schema_docs {
437 4     4   12 my ( $spec, $writer, $base ) = @_;
438 4   50     14 my $schemas = $spec->raw->{components}{schemas} // {};
439 4 50       14 return unless ref $schemas eq 'HASH';
440 4         18 for my $name ( sort keys %$schemas ) {
441 5         20 my $md = _schema_markdown( $spec, $base, $name, $schemas->{$name} );
442 5         23 $writer->write( "docs/$name.md", $md );
443             }
444             }
445              
446             sub generate {
447 4     4 1 29 my ( $class, %arg ) = @_;
448 4   33     17 my $writer = $arg{writer} // croak 'writer';
449 4   33     13 my $spec = $arg{spec} // croak 'spec';
450 4   33     14 my $base = $arg{base} // croak 'base';
451 4   50     10 my $client = $arg{client} // 1;
452 4   50     10 my $server = $arg{server} // 1;
453 4   50     12 my $ui = $arg{ui} // 1;
454 4   50     10 my $sync = $arg{sync} // 1;
455 4   50     12 my $async = $arg{async} // 1;
456 4   50     11 my $ui_only = $arg{ui_only} // 0;
457 4   50     11 my $local_test = $arg{local_test} // 0;
458              
459 4         14 my $title = $spec->title;
460 4   50     12 my $ver = $spec->raw->{info}{version} // '';
461 4         13 my $oav = $spec->openapi_version;
462 4   50     11 my $desc = $spec->raw->{info}{description} // '';
463 4         11 $desc =~ s/\r\n/\n/g;
464 4         7 $desc =~ s/\s+\z//;
465 4   50     10 my $genver = $Modern::OpenAPI::Generator::VERSION // '0';
466              
467 4         11 my $ops = $spec->operations;
468              
469 4 100 33     16 if ($client) {
    50          
470 3         15 _write_tag_api_docs( $writer, $spec, $base, $ops );
471 3         15 _write_schema_docs( $spec, $writer, $base );
472             }
473             elsif ( $server && $local_test ) {
474 1         3 _write_schema_docs( $spec, $writer, $base );
475             }
476              
477 4         11 my @rows;
478 4         21 for my $op (@$ops) {
479 4   50     63 my $sum = $op->{operation_hash}{summary} // '';
480 4         29 $sum =~ s/\|/\\\|/g;
481             push @rows,
482             sprintf '| %s | %s | %s | %s |',
483 4         21 $op->{method}, $op->{path}, $op->{operation_id}, $sum;
484             }
485 4 50       18 my $table = @rows ? join( "\n", @rows ) : '| — | — | — | — |';
486              
487 4         78 my @parts;
488              
489 4         12 push @parts, <<"HEAD";
490             # $title
491              
492             Perl modules under `$base` - HTTP client (Mojo), optional Mojolicious server, optional Swagger UI.
493              
494             HEAD
495              
496 4 50       13 if ( length $desc ) {
497 0         0 push @parts, "## Description\n\n$desc\n\n";
498             }
499              
500 4         14 push @parts, <<"VERSION";
501             ## VERSION
502              
503             - OpenAPI document version: `$oav`
504             - API version (info.version): `$ver`
505             - Generator: [Modern::OpenAPI::Generator](https://metacpan.org/) `$genver` (CLI: `oapi-perl-gen`)
506              
507             VERSION
508              
509 4         8 push @parts, <<'INSTALL';
510             ## Installation
511              
512             Dependencies are listed in `cpanfile`. From this directory:
513              
514             ```bash
515             cpanm --installdeps .
516             ```
517              
518             Or with Carton / your preferred tool.
519              
520             INSTALL
521              
522 4 100       24 if ($client) {
523 3         6 my $sync_pkg = "$base\::Client::Sync";
524 3         6 my $async_pkg = "$base\::Client::Async";
525 3         4 my $core_pkg = "$base\::Client::Core";
526 3 50       19 my $meth = @$ops ? _perl_method( $ops->[0]{operation_id} ) : 'operation';
527 3         6 my $ex = '';
528 3 50 33     19 if ( $sync && $async ) {
    0          
    0          
529 3         14 $ex = <<"EX";
530             ### Synchronous client (`$sync_pkg`)
531              
532             ```perl
533             use $sync_pkg;
534             use Path::Tiny qw(path);
535              
536             my \$core = $core_pkg->new(
537             base_url => 'https://api.example.com',
538             openapi_schema_file => path('share/openapi.yaml')->absolute->stringify,
539             );
540             my \$client = $sync_pkg->new( core => \$core );
541              
542             # First operation in spec as an example (method name is derived from operationId):
543             my \$r = \$client->$meth();
544             my \$data = \$r->data; # inflated from JSON when response has a schema \$ref
545             ```
546              
547             ### Asynchronous client (`$async_pkg`)
548              
549             ```perl
550             use $async_pkg;
551             # same \$core as above
552             my \$async = $async_pkg->new( core => \$core );
553             \$async->$meth()->then(sub ( \$r ) {
554             my \$data = \$r->data; # same ::Client::Result as sync; \$r->tx is the Mojo::Transaction
555             ...
556             });
557             ```
558              
559             EX
560             }
561             elsif ($sync) {
562 0         0 $ex = <<"EX";
563             ```perl
564             use $sync_pkg;
565             use Path::Tiny qw(path);
566              
567             my \$core = $core_pkg->new(
568             base_url => 'https://api.example.com',
569             openapi_schema_file => path('share/openapi.yaml')->absolute->stringify,
570             );
571             my \$client = $sync_pkg->new( core => \$core );
572             my \$r = \$client->$meth();
573             my \$data = \$r->data;
574             ```
575              
576             EX
577             }
578             elsif ($async) {
579 0         0 $ex = <<"EX";
580             ```perl
581             use $async_pkg;
582             use Path::Tiny qw(path);
583              
584             my \$core = $core_pkg->new(
585             base_url => 'https://api.example.com',
586             openapi_schema_file => path('share/openapi.yaml')->absolute->stringify,
587             );
588             my \$async = $async_pkg->new( core => \$core );
589             \$async->$meth()->then(sub ( \$r ) {
590             my \$data = \$r->data; # same ::Client::Result as sync; \$r->tx is the Mojo::Transaction
591             ...
592             });
593             ```
594              
595             EX
596             }
597              
598 3         7 push @parts, "## Client usage\n\n$ex\n";
599 3         7 push @parts, <<'CVAL';
600             ### Request validation (client)
601              
602             When `openapi_schema_file` is set on `::Client::Core`, [OpenAPI::Modern](https://metacpan.org/pod/OpenAPI::Modern) runs **`validate_request`** on the outgoing HTTP request **before** send, and **`validate_response`** on the response **before** inflating JSON into shared `::Model::*` objects. Either failure: synchronous calls **croak**, asynchronous **reject**, with a message that includes the validation result.
603              
604             CVAL
605             }
606              
607 4 50       9 if ($server) {
608 4 100       10 if ($local_test) {
609 2         5 push @parts, <<'SERVER';
610             ## Run the HTTP server (Mojolicious + OpenAPI)
611              
612             From the **generated project root** (where `share/` and `script/` live):
613              
614             ```bash
615             export MOJO_HOME="$PWD"
616             export PERL5LIB="$PWD/lib"
617             perl script/server.pl daemon -l 'http://127.0.0.1:3000'
618             ```
619              
620             - Replace host/port as needed.
621             - `MOJO_HOME` must point at this tree so `share/openapi.mojo.yaml` is found.
622             - This tree was generated with **`oapi-perl-gen --local-test`**: controller actions call **`$c->openapi->valid_input`**, then **`::StubData->for_operation`** builds a random payload from the first 2xx `application/json` response schema, inflates it with **`::Model::*->from_json`** when a model exists, and **`render(json => ...)`** serializes via **`TO_JSON`**. Replace with real logic when you are ready.
623             - **Swagger UI** (with `--ui`, on by default when you generate the full stack) is on the **same** app and port: **http://127.0.0.1:3000/swagger** — API paths from the spec are on the same origin.
624              
625             SERVER
626             }
627             else {
628 2         5 push @parts, <<'SERVER';
629             ## Run the HTTP server (Mojolicious + OpenAPI)
630              
631             From the **generated project root** (where `share/` and `script/` live):
632              
633             ```bash
634             export MOJO_HOME="$PWD"
635             export PERL5LIB="$PWD/lib"
636             perl script/server.pl daemon -l 'http://127.0.0.1:3000'
637             ```
638              
639             - Replace host/port as needed.
640             - `MOJO_HOME` must point at this tree so `share/openapi.mojo.yaml` is found.
641             - Default routes follow the spec; each action starts with **`$c->openapi->valid_input`** so the **incoming request** is checked against the spec (invalid requests get **400**). Controller stubs then return **501** until you implement them (or regenerate with **`oapi-perl-gen --local-test`** for **`StubData`** + **`Model::*`** responses).
642             - **Swagger UI** (with `--ui`, on by default when you generate the full stack) is on the **same** app and port: **http://127.0.0.1:3000/swagger** — API paths from the spec are on the same origin.
643              
644             SERVER
645             }
646              
647 4 100       13 if ($ui) {
648 3         5 push @parts, <<'UI';
649             - OpenAPI for Swagger: **http://127.0.0.1:3000/openapi.yaml** — by default the YAML `servers` list is **unchanged** from the spec. To **prepend the current request origin** (so **Try it out** targets the daemon you opened Swagger on, any `-l` port), run the app with **`--local-test`** on **`script/server.pl`** (not the `oapi-perl-gen` flag), e.g. `perl script/server.pl daemon -l 'http://127.0.0.1:3000' --local-test`, or set **`OAPI_SWAGGER_LOCAL_ORIGIN=1`**.
650              
651             UI
652             }
653              
654 4         8 push @parts, <<'MORBO';
655             ### Development server (reload on change)
656              
657             If `morbo` is available (ships with Mojolicious):
658              
659             ```bash
660             export MOJO_HOME="$PWD"
661             export PERL5LIB="$PWD/lib"
662             morbo script/server.pl -l 'http://127.0.0.1:3000'
663             ```
664              
665             MORBO
666             }
667              
668 4 50       12 if ($ui_only) {
669 0         0 push @parts, <<'UIONLY';
670             ## Spec browser only (`--no-server --ui`)
671              
672             The same **`script/server.pl`** runs a minimal Mojolicious app: `share/openapi.yaml` on disk plus Swagger UI — **no** OpenAPI-driven API routes.
673              
674             ```bash
675             export MOJO_HOME="$PWD"
676             export PERL5LIB="$PWD/lib"
677             perl script/server.pl daemon -l 'http://127.0.0.1:3000'
678             ```
679              
680             - Swagger UI: **http://127.0.0.1:3000/swagger**
681             - `/openapi.yaml` prepends the request origin to `servers` only when **`script/server.pl` is run with `--local-test`** (or `OAPI_SWAGGER_LOCAL_ORIGIN=1`), same as the full server.
682              
683             UIONLY
684             }
685              
686 4         8 push @parts, <<'TESTS';
687             ## Tests (generated smoke / load)
688              
689             ```bash
690             prove -l t
691             ```
692              
693             TESTS
694              
695 4         8 my $layout_docs = '';
696 4 100 33     23 if ($client) {
    50          
697 3         5 $layout_docs =
698             "| `docs/` | Per-tag API docs (`*Api.md`) and `components/schemas` model docs (`*.md`) |\n";
699             }
700             elsif ( $server && $local_test ) {
701 1         5 $layout_docs =
702             "| `docs/` | `components/schemas` model docs (`*.md`) (with `--local-test` server, models are generated even if `--no-client`) |\n";
703             }
704              
705 4         14 push @parts, <<"OPS";
706             ## Operations
707              
708             | HTTP | Path | operationId | Summary |
709             |------|------|-------------|---------|
710             $table
711              
712             ## Layout
713              
714             | Path | Purpose |
715             |------|---------|
716             | `lib/` | `::Client::*`, `::Server::*`, shared `::Model::*` (OpenAPI schemas), optional `::StubData` (`--local-test` server) |
717             | `share/` | `openapi.yaml` (copy of spec); with full server also `openapi.mojo.yaml` for Mojolicious::Plugin::OpenAPI |
718             | `script/server.pl` | Single entrypoint: full API + Swagger UI at `/swagger`, or spec-only mode when generated with `--no-server --ui` |
719             ${layout_docs}| `t/` | Tests for **this generated tree** |
720             | `cpanfile` | Runtime and test dependencies |
721              
722             OPS
723              
724 4         16 push @parts, _documentation_for_api_endpoints( $spec, $ops, $client );
725              
726 4 50 33     26 push @parts, _documentation_for_models( $spec, $base )
      66        
727             if $client || ( $server && $local_test );
728              
729 4         73 $writer->write( 'README.md', join '', @parts );
730             }
731              
732             1;
733              
734             __END__