File Coverage

blib/lib/OpenAPI/Render.pm
Criterion Covered Total %
statement 90 99 90.9
branch 25 32 78.1
condition 12 18 66.6
subroutine 16 22 72.7
pod 15 15 100.0
total 158 186 84.9


line stmt bran cond sub pod time code
1             package OpenAPI::Render;
2              
3 1     1   6 use strict;
  1         1  
  1         27  
4 1     1   4 use warnings;
  1         2  
  1         26  
5              
6 1     1   460 use Clone qw( clone );
  1         2520  
  1         65  
7 1     1   685 use JSON qw( decode_json );
  1         12635  
  1         7  
8 1     1   535 use version;
  1         1725  
  1         8  
9              
10             # ABSTRACT: Render OpenAPI specifications as documents
11             our $VERSION = '0.3.0'; # VERSION
12              
13             =head1 DESCRIPTION
14              
15             C is a class meant to be subclassed and used to render OpenAPI specifications.
16             Currently OpenAPI version 3.0.2 is the target version, but in principle all 3.* versions should work.
17             C provides methods for representing OpenAPI definitions such as operations and parameters, and the base class performs the required traversal.
18             Thus it should be enough to subclass it and override the appropriate methods.
19             For examples see L and L.
20              
21             =head1 MAIN METHODS
22              
23             =head2 C
24              
25             Given an OpenAPI specification in raw JSON or parsed data structure, constructs a C object.
26             Does not modify input values.
27              
28             =cut
29              
30             sub new
31             {
32 1     1 1 463 my( $class, $api ) = @_;
33              
34 1 50       6 if( ref $api ) {
35             # Parsed JSON given, need to make a copy as dereferencing will modify it.
36 0         0 $api = clone $api;
37             } else {
38             # Raw JSON given, need to parse.
39 1         233 $api = decode_json $api;
40             }
41              
42 1         7 my $self = { api => _dereference( $api, $api ) };
43              
44 1 50       8 if( exists $self->{api}{openapi} ) {
45 1         16 my $version = version->parse( $self->{api}{openapi} );
46 1 50 33     24 if( $version < version->parse( '3' ) || $version > version->parse( '4' ) ) {
47 0         0 warn "unsupported OpenAPI version $self->{api}{openapi}, " .
48             'results may be incorrect', "\n";
49             }
50             } else {
51 0         0 warn 'top-level attribute "openapi" not found, cannot ensure ' .
52             'this is OpenAPI, cannot check version', "\n";
53             }
54              
55 1         5 my( $base_url ) = map { $_->{url} } @{$api->{servers} };
  1         5  
  1         55  
56 1 50       5 $self->{base_url} = $base_url if $base_url;
57              
58 1         5 return bless $self, $class;
59             }
60              
61             =head2 C
62              
63             Main generating method (does not take any parameters).
64             Returns a string with rendered representation of an OpenAPI specification.
65              
66             =cut
67              
68             sub show
69             {
70 1     1 1 50 my( $self ) = @_;
71              
72 1         9 my $html = $self->header;
73 1         7681 my $api = $self->api;
74              
75 1         3 for my $path (sort keys %{$api->{paths}}) {
  1         10  
76 3         268 $html .= $self->path_header( $path );
77 3         131 for my $operation ('get', 'post', 'patch', 'put', 'delete') {
78 15 100       2179 next if !$api->{paths}{$path}{$operation};
79 10         32 my @parameters = $self->parameters( $path, $operation );
80 10         21 my $responses = $api->{paths}{$path}{$operation}{responses};
81              
82             $html .= $self->operation_header( $path, $operation ) .
83              
84             $self->parameters_header .
85 74         194 join( '', map { $self->parameter( $_ ) } @parameters ) .
86             $self->parameters_footer .
87              
88             $self->responses_header .
89 10         32 join( '', map { $self->response( $_, $responses->{$_} ) }
  20         53  
90             sort keys %$responses ) .
91             $self->responses_footer .
92              
93             $self->operation_footer( $path, $operation );
94             }
95             }
96              
97 1         12 $html .= $self->footer;
98 1         76 return $html;
99             }
100              
101             =head1 RENDERING METHODS
102              
103             =head2 C
104              
105             Text added before everything else.
106             Empty in the base class.
107              
108             =cut
109              
110 0     0 1 0 sub header { return '' }
111              
112             =head2 C
113              
114             Text added after everything else.
115             Empty in the base class.
116              
117             =cut
118              
119 0     0 1 0 sub footer { return '' }
120              
121             =head2 C
122              
123             Text added before each path.
124             Empty in the base class.
125              
126             =cut
127              
128 0     0 1 0 sub path_header { return '' }
129              
130             =head2 C
131              
132             Text added before each operation.
133             Empty in the base class.
134              
135             =cut
136              
137 0     0 1 0 sub operation_header { return '' }
138              
139             =head2 C
140              
141             Text added before parameters list.
142             Empty in the base class.
143              
144             =cut
145              
146 10     10 1 362 sub parameters_header { return '' };
147              
148             =head2 C
149              
150             Returns representation of a single parameter.
151             Empty in the base class.
152              
153             =cut
154              
155 0     0 1 0 sub parameter { return '' }
156              
157             =head2 C
158              
159             Text added after parameters list.
160             Empty in the base class.
161              
162             =cut
163              
164 10     10 1 30 sub parameters_footer { return '' };
165              
166             =head2 C
167              
168             Text added before responses list.
169             Empty in the base class.
170              
171             =cut
172              
173 10     10 1 51 sub responses_header { return '' };
174              
175             =head2 C
176              
177             Returns representation of a single response.
178             Empty in the base class.
179              
180             =cut
181              
182 20     20 1 58 sub response { return '' };
183              
184             =head2 C
185              
186             Text added after responses list.
187             Empty in the base class.
188              
189             =cut
190              
191 10     10 1 29 sub responses_footer { return '' };
192              
193             =head2 C
194              
195             Text added after each operation.
196             Empty in the base class.
197              
198             =cut
199              
200 0     0 1 0 sub operation_footer { return '' }
201              
202             =head1 HELPER METHODS
203              
204             =head2 C
205              
206             Returns the parsed and dereferenced input OpenAPI specification.
207             Note that in the returned data structure all references are dereferenced, i.e., flat.
208              
209             =cut
210              
211             sub api
212             {
213 39     39 1 3124 my( $self ) = @_;
214 39         129 return $self->{api};
215             }
216              
217             =head2 C
218              
219             Returns the list of parameters.
220             Optionally, path and operation can be given to filter the parameters.
221             Note that object-typed schemas from C are translated to parameters too.
222              
223             =cut
224              
225             sub parameters
226             {
227 16     16 1 34 my( $self, $path_filter, $operation_filter ) = @_;
228              
229 16         29 my $api = $self->api;
230              
231 16         25 my @parameters;
232 16         22 for my $path (sort keys %{$api->{paths}}) {
  16         64  
233 48 100 100     183 next if $path_filter && $path ne $path_filter;
234 18         36 for my $operation ('get', 'post', 'patch', 'put', 'delete') {
235 90 100 100     234 next if $operation_filter && $operation ne $operation_filter;
236 30 100       70 next if !$api->{paths}{$path}{$operation};
237              
238 25 50       63 if( exists $api->{paths}{$path}{parameters} ) {
239 25         33 push @parameters, @{$api->{paths}{$path}{parameters}};
  25         53  
240             }
241              
242 25 100       68 if( exists $api->{paths}{$path}{$operation}{parameters} ) {
243 6         9 push @parameters, @{$api->{paths}{$path}{$operation}{parameters}};
  6         17  
244             }
245              
246 25 100       54 if( exists $api->{paths}{$path}{$operation}{requestBody} ) {
247             push @parameters,
248 14         27 _RequestBody2Parameters( $api->{paths}{$path}{$operation}{requestBody} );
249             }
250             }
251             }
252              
253 16         93 return @parameters;
254             }
255              
256             sub _dereference
257             {
258 307     307   474 my( $node, $root ) = @_;
259              
260 307 100       628 if( ref $node eq 'ARRAY' ) {
    100          
261 11         18 @$node = map { _dereference( $_, $root ) } @$node;
  43         71  
262             } elsif( ref $node eq 'HASH' ) {
263 162         384 my @keys = keys %$node;
264 162 100 100     400 if( scalar @keys == 1 && $keys[0] eq '$ref' ) {
265 37         94 my @path = split '/', $node->{'$ref'};
266 37         52 shift @path;
267 37         52 $node = $root;
268 37         65 while( @path ) {
269 111         228 $node = $node->{shift @path};
270             }
271             } else {
272 125         181 %$node = map { $_ => _dereference( $node->{$_}, $root ) } @keys;
  263         405  
273             }
274             }
275 307         701 return $node;
276             }
277              
278             sub _RequestBody2Parameters
279             {
280 14     14   26 my( $requestBody ) = @_;
281              
282             return if !exists $requestBody->{content} ||
283             !exists $requestBody->{content}{'multipart/form-data'} ||
284 14 50 33     69 !exists $requestBody->{content}{'multipart/form-data'}{schema};
      33        
285              
286 14         23 my $schema = $requestBody->{content}{'multipart/form-data'}{schema};
287              
288 14 50       28 return if $schema->{type} ne 'object';
289             return ( map { {
290             in => 'query',
291             name => $_,
292 84         226 schema => $schema->{properties}{$_} } }
293 14         49 sort keys %{$schema->{properties}} ),
294             ( map { {
295             in => 'query',
296             name => $_,
297 14         78 schema => $schema->{patternProperties}{$_},
298             'x-is-pattern' => 1 } }
299 14         19 sort keys %{$schema->{patternProperties}} );
  14         45  
300             }
301              
302             1;