File Coverage

blib/lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm
Criterion Covered Total %
statement 94 94 100.0
branch 36 40 90.0
condition 23 29 79.3
subroutine 20 20 100.0
pod 1 1 100.0
total 174 184 94.5


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::OpenAPI::SpecRenderer;
2 49     49   30868 use Mojo::Base 'Mojolicious::Plugin';
  49         179  
  49         388  
3              
4 49     49   9488 use JSON::Validator;
  49         35279  
  49         410  
5 49     49   1545 use Mojo::JSON;
  49         152  
  49         2885  
6 49     49   382 use Scalar::Util qw(blessed);
  49         141  
  49         3523  
7              
8 49   50 49   376 use constant DEBUG => $ENV{MOJO_OPENAPI_DEBUG} || 0;
  49         160  
  49         4229  
9 49     49   428 use constant MARKDOWN => eval 'require Text::Markdown;1';
  49         128  
  49         5492  
10              
11             sub register {
12 60     60 1 1015 my ($self, $app, $config) = @_;
13              
14 60         434 $app->defaults(openapi_spec_renderer_logo => '/mojolicious/plugin/openapi/logo.png');
15 60         1590 $app->defaults(openapi_spec_renderer_theme_color => '#508a25');
16              
17 60 100       1418 $self->{standalone} = $config->{openapi} ? 0 : 1;
18 60     34   528 $app->helper('openapi.render_spec' => sub { $self->_render_spec(@_) });
  34         237233  
19 60         41740 $app->helper('openapi.rich_text' => \&_helper_rich_text);
20              
21             # EXPERIMENTAL
22 60         38313 $app->helper('openapi.spec_iterator' => \&_helper_iterator);
23              
24 60 100       39741 unless ($app->{'openapi.render_specification'}++) {
25 52         189 push @{$app->renderer->classes}, __PACKAGE__;
  52         219  
26 52         765 push @{$app->static->classes}, __PACKAGE__;
  52         426  
27             }
28              
29 60 100       1308 $self->_register_with_openapi($app, $config) unless $self->{standalone};
30             }
31              
32             sub _helper_iterator {
33 108     108   60234 my ($c, $obj) = @_;
34 108 50       305 return unless $obj;
35              
36 108 100       394 unless ($c->{_helper_iterator}{$obj}) {
37 29         126 my $x_re = qr{^x-};
38             $c->{_helper_iterator}{$obj}
39 29         222 = [map { [$_, $obj->{$_}] } sort { lc $a cmp lc $b } grep { !/$x_re/ } keys %$obj];
  79         314  
  112         255  
  79         394  
40             }
41              
42 108         260 my $items = $c->{_helper_iterator}{$obj};
43 108         218 my $item = shift @$items;
44 108 100       304 delete $c->{_helper_iterator}{$obj} unless $item;
45 108 100       664 return $item ? @$item : ();
46             }
47              
48             sub _register_with_openapi {
49 59     59   199 my ($self, $app, $config) = @_;
50 59         163 my $openapi = $config->{openapi};
51              
52 59 50 50     497 if ($config->{render_specification} // 1) {
53             my $spec_route = $openapi->route->get(
54             '/',
55             [format => [qw(html json)]],
56             {format => undef},
57 27     27   497862 sub { shift->openapi->render_spec(@_) }
58 59         304 );
59 59   100     18853 my $name = $config->{spec_route_name} || $openapi->validator->get('/x-mojo-name');
60 59 100       6138 $spec_route->name($name) if $name;
61             }
62              
63 59 50 50     653 if ($config->{render_specification_for_paths} // 1) {
64 59     57   338 $app->plugins->once(openapi_routes_added => sub { $self->_add_documentation_routes(@_) });
  57         3846  
65             }
66             }
67              
68             sub _add_documentation_routes {
69 57     57   208 my ($self, $openapi, $routes) = @_;
70 57         142 my %dups;
71              
72 57         169 for my $route (@$routes) {
73 125         490 my $route_path = $route->to_string;
74 125 100       5909 next if $dups{$route_path}++;
75              
76 113         400 my $openapi_path = $route->to->{'openapi.path'};
77 113         1562 my $doc_route
78             = $openapi->route->options($route->pattern->unparsed, {'openapi.default_options' => 1});
79 113     12   40056 $doc_route->to(cb => sub { $self->_render_spec(shift, $openapi_path) });
  12         194435  
80 113 50       2962 $doc_route->name(join '_', $route->name, 'openapi_documentation') if $route->name;
81 113         2944 warn "[OpenAPI] Add route options $route_path (@{[$doc_route->name // '']})\n" if DEBUG;
82             }
83             }
84              
85             sub _helper_rich_text {
86 63     63   26059 return Mojo::ByteStream->new(MARKDOWN ? Text::Markdown::markdown($_[1]) : $_[1]);
87             }
88              
89             sub _render_partial_spec {
90 14     14   65 my ($self, $c, $path, $custom) = @_;
91 14         90 my $method = $c->param('method');
92 14   66     3806 my $validator = $custom || Mojolicious::Plugin::OpenAPI::_self($c)->validator;
93              
94 14 100       210 return $c->render(json => {errors => [{message => 'No spec defined.'}]}, status => 404)
    100          
95             unless my $schema = $validator->get([paths => $path, $method ? ($method) : ()]);
96              
97             return $c->render(
98             json => {
99             '$schema' => 'http://json-schema.org/draft-04/schema#',
100             title => $validator->get([qw(info title)]) || '',
101             description => $validator->get([qw(info description)]) || '',
102 13 100 50     2075 %{$validator->bundle({schema => $schema})->data},
  13   100     2851  
103             $method ? (parameters => $validator->parameters_for_request([$method, $path])) : (),
104             }
105             );
106             }
107              
108             sub _render_spec {
109 46     46   197 my ($self, $c, $path, $custom) = @_;
110 46 100       238 return $self->_render_partial_spec($c, $path, $custom) if $path;
111              
112 32 100 100     364 my $openapi = $custom || $self->{standalone} ? undef : Mojolicious::Plugin::OpenAPI::_self($c);
113 32   100     161 my $format = $c->stash('format') || 'json';
114 32         419 my $validator = $custom;
115              
116 32 100 100     262 if (!$validator and $openapi) {
117 29   66     227 $validator = $openapi->{bundled} ||= $openapi->validator->bundle;
118 29         443832 $validator->base_url($c->req->url->to_abs->path($c->url_for($validator->base_url->path)));
119             }
120              
121 32 100       45383 return $c->render(json => {errors => [{message => 'No specification to render.'}]}, status => 500)
122             unless $validator;
123              
124             my $operations = $validator->routes->each(sub {
125 58     58   12656 my $path_item = $_;
126 58         285 $path_item->{op_spec} = $validator->get([paths => @$path_item{qw(path method)}]);
127 58   100     7390 $path_item->{operation_id} //= '';
128 58         185 $path_item;
129 31         207 });
130              
131 31 100       336 return $c->render(json => $validator->data) unless $format eq 'html';
132             return $c->render(
133             base_url => $validator->base_url,
134             handler => 'ep',
135             template => 'mojolicious/plugin/openapi/layout',
136 6         606 operations => [sort { $a->{operation_id} cmp $b->{operation_id} } @$operations],
137             serialize => \&_serialize,
138             slugify => sub {
139 42     42   91176 join '-', map { s/\W/-/g; lc } map {"$_"} @_;
  126         343  
  126         433  
  126         396  
140             },
141 5         32 spec => $validator->data,
142             );
143             }
144              
145 66     66   75873 sub _serialize { Mojo::JSON::encode_json(@_) }
146              
147             1;
148              
149             =encoding utf8
150              
151             =head1 NAME
152              
153             Mojolicious::Plugin::OpenAPI::SpecRenderer - Render OpenAPI specification
154              
155             =head1 SYNOPSIS
156              
157             =head2 With Mojolicious::Plugin::OpenAPI
158              
159             $app->plugin(OpenAPI => {
160             plugins => [qw(+SpecRenderer)],
161             render_specification => 1,
162             render_specification_for_paths => 1,
163             %openapi_parameters,
164             });
165              
166             See L for what
167             C<%openapi_parameters> might contain.
168              
169             =head2 Standalone
170              
171             use Mojolicious::Lite;
172             plugin "Mojolicious::Plugin::OpenAPI::SpecRenderer";
173              
174             # Some specification to render
175             my $petstore = app->home->child("petstore.json");
176              
177             get "/my-spec" => sub {
178             my $c = shift;
179             my $path = $c->param('path') || '/';
180             state $custom = JSON::Validator->new->schema($petstore->to_string)->schema->bundle;
181             $c->openapi->render_spec($path, $custom);
182             };
183              
184             =head1 DESCRIPTION
185              
186             L will enable
187             L to render the specification in both HTML and
188             JSON format. It can also be used L if you just want to render
189             the specification, and not add any API routes to your application.
190              
191             See L to see how you can override parts of the rendering.
192              
193             The human readable format focus on making the documentation printable, so you
194             can easily share it with third parties as a PDF. If this documentation format
195             is too basic or has missing information, then please
196             L
197             suggestions for enhancements.
198              
199             See L for a demo.
200              
201             =head1 HELPERS
202              
203             =head2 openapi.render_spec
204              
205             $c = $c->openapi->render_spec;
206             $c = $c->openapi->render_spec($json_path);
207             $c = $c->openapi->render_spec($json_path, $openapi_v2_schema_object);
208             $c = $c->openapi->render_spec("/user/{id}");
209              
210             Used to render the specification as either "html" or "json". Set the
211             L variable "format" to change the format to render.
212              
213             Will render the whole specification by default, but can also render
214             documentation for a given OpenAPI path.
215              
216             =head2 openapi.rich_text
217              
218             $bytestream = $c->openapi->rich_text($text);
219              
220             Used to render the "description" in the specification with L if
221             it is installed. Will just return the text if the module is not available.
222              
223             =head1 METHODS
224              
225             =head2 register
226              
227             $doc->register($app, $openapi, \%config);
228              
229             Adds the features mentioned in the L.
230              
231             C<%config> is the same as passed on to
232             L. The following keys are used by this
233             plugin:
234              
235             =head3 render_specification
236              
237             Render the whole specification as either HTML or JSON from "/:basePath".
238             Example if C in your specification is "/api":
239              
240             GET https://api.example.com/api.html
241             GET https://api.example.com/api.json
242              
243             Disable this feature by setting C to C<0>.
244              
245             =head3 render_specification_for_paths
246              
247             Render the specification from individual routes, using the OPTIONS HTTP method.
248             Example:
249              
250             OPTIONS https://api.example.com/api/some/path.json
251             OPTIONS https://api.example.com/api/some/path.json?method=post
252              
253             Disable this feature by setting C to C<0>.
254              
255             =head1 TEMPLATING
256              
257             Overriding templates is EXPERIMENTAL, but not very likely to break in a bad
258             way.
259              
260             L uses many template files to make
261             up the human readable version of the spec. Each of them can be overridden by
262             creating a file in your templates folder.
263              
264             mojolicious/plugin/openapi/layout.html.ep
265             |- mojolicious/plugin/openapi/head.html.ep
266             | '- mojolicious/plugin/openapi/style.html.ep
267             |- mojolicious/plugin/openapi/header.html.ep
268             | |- mojolicious/plugin/openapi/logo.html.ep
269             | '- mojolicious/plugin/openapi/toc.html.ep
270             |- mojolicious/plugin/openapi/intro.html.ep
271             |- mojolicious/plugin/openapi/resources.html.ep
272             | '- mojolicious/plugin/openapi/resource.html.ep
273             | |- mojolicious/plugin/openapi/human.html.ep
274             | |- mojolicious/plugin/openapi/parameters.html.ep
275             | '- mojolicious/plugin/openapi/response.html.ep
276             | '- mojolicious/plugin/openapi/human.html.ep
277             |- mojolicious/plugin/openapi/references.html.ep
278             |- mojolicious/plugin/openapi/footer.html.ep
279             |- mojolicious/plugin/openapi/javascript.html.ep
280             '- mojolicious/plugin/openapi/foot.html.ep
281              
282             See the DATA section in the source code for more details on styling and markup
283             structure.
284              
285             L
286              
287             Variables available in the templates:
288              
289             %= $serialize->($data_structure)
290             %= $slugify->(@str)
291             %= $spec->{info}{title}
292              
293             In addition, there is a logo in "header.html.ep" that can be overridden by
294             either changing the static file "mojolicious/plugin/openapi/logo.png" or set
295             "openapi_spec_renderer_logo" in L to a
296             custom URL.
297              
298             =head1 SEE ALSO
299              
300             L
301              
302             =cut
303              
304             __DATA__