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   1084563 use Mojo::Base 'Mojolicious::Plugin';
  49         133  
  49         420  
3              
4 49     49   16997 use JSON::Validator;
  49         43463  
  49         411  
5 49     49   1658 use Mojo::JSON;
  49         689  
  49         3938  
6 49     49   499 use Scalar::Util qw(blessed);
  49         239  
  49         3926  
7              
8 49   50 49   332 use constant DEBUG => $ENV{MOJO_OPENAPI_DEBUG} || 0;
  49         131  
  49         5741  
9 49     49   335 use constant MARKDOWN => eval 'require Text::Markdown;1';
  49         106  
  49         7133  
10              
11             sub register {
12 60     60 1 1255 my ($self, $app, $config) = @_;
13              
14 60         572 $app->defaults(openapi_spec_renderer_logo => '/mojolicious/plugin/openapi/logo.png');
15 60         1915 $app->defaults(openapi_spec_renderer_theme_color => '#508a25');
16              
17 60 100       1670 $self->{standalone} = $config->{openapi} ? 0 : 1;
18 60     34   598 $app->helper('openapi.render_spec' => sub { $self->_render_spec(@_) });
  34         350370  
19 60         50408 $app->helper('openapi.rich_text' => \&_helper_rich_text);
20              
21             # EXPERIMENTAL
22 60         54805 $app->helper('openapi.spec_iterator' => \&_helper_iterator);
23              
24 60 100       46334 unless ($app->{'openapi.render_specification'}++) {
25 52         250 push @{$app->renderer->classes}, __PACKAGE__;
  52         264  
26 52         738 push @{$app->static->classes}, __PACKAGE__;
  52         458  
27             }
28              
29 60 100       1537 $self->_register_with_openapi($app, $config) unless $self->{standalone};
30             }
31              
32             sub _helper_iterator {
33 108     108   73048 my ($c, $obj) = @_;
34 108 50       328 return unless $obj;
35              
36 108 100       420 unless ($c->{_helper_iterator}{$obj}) {
37 29         135 my $x_re = qr{^x-};
38             $c->{_helper_iterator}{$obj}
39 29         189 = [map { [$_, $obj->{$_}] } sort { lc $a cmp lc $b } grep { !/$x_re/ } keys %$obj];
  79         293  
  103         291  
  79         422  
40             }
41              
42 108         246 my $items = $c->{_helper_iterator}{$obj};
43 108         213 my $item = shift @$items;
44 108 100       296 delete $c->{_helper_iterator}{$obj} unless $item;
45 108 100       525 return $item ? @$item : ();
46             }
47              
48             sub _register_with_openapi {
49 59     59   195 my ($self, $app, $config) = @_;
50 59         200 my $openapi = $config->{openapi};
51              
52 59 50 50     570 if ($config->{render_specification} // 1) {
53             my $spec_route = $openapi->route->get(
54             '/',
55             [format => [qw(html json)]],
56             {format => undef},
57 27     27   480387 sub { shift->openapi->render_spec(@_) }
58 59         343 );
59 59   100     22867 my $name = $config->{spec_route_name} || $openapi->validator->get('/x-mojo-name');
60 59 100       8661 $spec_route->name($name) if $name;
61             }
62              
63 59 50 50     552 if ($config->{render_specification_for_paths} // 1) {
64 59     57   334 $app->plugins->once(openapi_routes_added => sub { $self->_add_documentation_routes(@_) });
  57         6238  
65             }
66             }
67              
68             sub _add_documentation_routes {
69 57     57   262 my ($self, $openapi, $routes) = @_;
70 57         147 my %dups;
71              
72 57         233 for my $route (@$routes) {
73 125         524 my $route_path = $route->to_string;
74 125 100       6295 next if $dups{$route_path}++;
75              
76 113         387 my $openapi_path = $route->to->{'openapi.path'};
77 113         1643 my $doc_route
78             = $openapi->route->options($route->pattern->unparsed, {'openapi.default_options' => 1});
79 113     12   47099 $doc_route->to(cb => sub { $self->_render_spec(shift, $openapi_path) });
  12         210298  
80 113 50       3608 $doc_route->name(join '_', $route->name, 'openapi_documentation') if $route->name;
81 113         2643 warn "[OpenAPI] Add route options $route_path (@{[$doc_route->name // '']})\n" if DEBUG;
82             }
83             }
84              
85             sub _helper_rich_text {
86 63     63   26125 return Mojo::ByteStream->new(MARKDOWN ? Text::Markdown::markdown($_[1]) : $_[1]);
87             }
88              
89             sub _render_partial_spec {
90 14     14   55 my ($self, $c, $path, $custom) = @_;
91 14         78 my $method = $c->param('method');
92 14   66     4668 my $validator = $custom || Mojolicious::Plugin::OpenAPI::_self($c)->validator;
93              
94 14 100       223 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     2830 %{$validator->bundle({schema => $schema})->data},
  13   100     2960  
103             $method ? (parameters => $validator->parameters_for_request([$method, $path])) : (),
104             }
105             );
106             }
107              
108             sub _render_spec {
109 46     46   193 my ($self, $c, $path, $custom) = @_;
110 46 100       255 return $self->_render_partial_spec($c, $path, $custom) if $path;
111              
112 32 100 100     476 my $openapi = $custom || $self->{standalone} ? undef : Mojolicious::Plugin::OpenAPI::_self($c);
113 32   100     125 my $format = $c->stash('format') || 'json';
114 32         420 my $validator = $custom;
115              
116 32 100 100     298 if (!$validator and $openapi) {
117 29   66     306 $validator = $openapi->{bundled} ||= $openapi->validator->bundle;
118 29         499748 $validator->base_url($c->req->url->to_abs->path($c->url_for($openapi->validator->base_url->path)));
119             }
120              
121 32 100       52206 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   14083 my $path_item = $_;
126 58         318 $path_item->{op_spec} = $validator->get([paths => @$path_item{qw(path method)}]);
127 58   100     8471 $path_item->{operation_id} //= '';
128 58         194 $path_item;
129 31         286 });
130              
131 31 100       381 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         614 operations => [sort { $a->{operation_id} cmp $b->{operation_id} } @$operations],
137             serialize => \&_serialize,
138             slugify => sub {
139 42     42   126106 join '-', map { s/\W/-/g; lc } map {"$_"} @_;
  126         364  
  126         405  
  126         334  
140             },
141 5         33 spec => $validator->data,
142             );
143             }
144              
145 66     66   105521 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__