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   31111 use Mojo::Base 'Mojolicious::Plugin';
  49         165  
  49         353  
3              
4 49     49   9356 use JSON::Validator;
  49         36516  
  49         389  
5 49     49   1491 use Mojo::JSON;
  49         130  
  49         2836  
6 49     49   431 use Scalar::Util qw(blessed);
  49         131  
  49         3421  
7              
8 49   50 49   358 use constant DEBUG => $ENV{MOJO_OPENAPI_DEBUG} || 0;
  49         139  
  49         4319  
9 49     49   409 use constant MARKDOWN => eval 'require Text::Markdown;1';
  49         185  
  49         5353  
10              
11             sub register {
12 60     60 1 1005 my ($self, $app, $config) = @_;
13              
14 60         378 $app->defaults(openapi_spec_renderer_logo => '/mojolicious/plugin/openapi/logo.png');
15 60         1492 $app->defaults(openapi_spec_renderer_theme_color => '#508a25');
16              
17 60 100       1467 $self->{standalone} = $config->{openapi} ? 0 : 1;
18 60     34   507 $app->helper('openapi.render_spec' => sub { $self->_render_spec(@_) });
  34         221582  
19 60         41795 $app->helper('openapi.rich_text' => \&_helper_rich_text);
20              
21             # EXPERIMENTAL
22 60         38677 $app->helper('openapi.spec_iterator' => \&_helper_iterator);
23              
24 60 100       40834 unless ($app->{'openapi.render_specification'}++) {
25 52         1874 push @{$app->renderer->classes}, __PACKAGE__;
  52         230  
26 52         700 push @{$app->static->classes}, __PACKAGE__;
  52         397  
27             }
28              
29 60 100       1186 $self->_register_with_openapi($app, $config) unless $self->{standalone};
30             }
31              
32             sub _helper_iterator {
33 108     108   59873 my ($c, $obj) = @_;
34 108 50       314 return unless $obj;
35              
36 108 100       372 unless ($c->{_helper_iterator}{$obj}) {
37 29         130 my $x_re = qr{^x-};
38             $c->{_helper_iterator}{$obj}
39 29         226 = [map { [$_, $obj->{$_}] } sort { lc $a cmp lc $b } grep { !/$x_re/ } keys %$obj];
  79         415  
  113         242  
  79         439  
40             }
41              
42 108         278 my $items = $c->{_helper_iterator}{$obj};
43 108         207 my $item = shift @$items;
44 108 100       293 delete $c->{_helper_iterator}{$obj} unless $item;
45 108 100       512 return $item ? @$item : ();
46             }
47              
48             sub _register_with_openapi {
49 59     59   207 my ($self, $app, $config) = @_;
50 59         168 my $openapi = $config->{openapi};
51              
52 59 50 50     451 if ($config->{render_specification} // 1) {
53             my $spec_route = $openapi->route->get(
54             '/',
55             [format => [qw(html json)]],
56             {format => undef},
57 27     27   494445 sub { shift->openapi->render_spec(@_) }
58 59         276 );
59 59   100     18196 my $name = $config->{spec_route_name} || $openapi->validator->get('/x-mojo-name');
60 59 100       6239 $spec_route->name($name) if $name;
61             }
62              
63 59 50 50     604 if ($config->{render_specification_for_paths} // 1) {
64 59     57   309 $app->plugins->once(openapi_routes_added => sub { $self->_add_documentation_routes(@_) });
  57         3664  
65             }
66             }
67              
68             sub _add_documentation_routes {
69 57     57   188 my ($self, $openapi, $routes) = @_;
70 57         141 my %dups;
71              
72 57         174 for my $route (@$routes) {
73 125         435 my $route_path = $route->to_string;
74 125 100       5978 next if $dups{$route_path}++;
75              
76 113         388 my $openapi_path = $route->to->{'openapi.path'};
77 113         1580 my $doc_route
78             = $openapi->route->options($route->pattern->unparsed, {'openapi.default_options' => 1});
79 113     12   39460 $doc_route->to(cb => sub { $self->_render_spec(shift, $openapi_path) });
  12         194675  
80 113 50       3065 $doc_route->name(join '_', $route->name, 'openapi_documentation') if $route->name;
81 113         2594 warn "[OpenAPI] Add route options $route_path (@{[$doc_route->name // '']})\n" if DEBUG;
82             }
83             }
84              
85             sub _helper_rich_text {
86 63     63   26245 return Mojo::ByteStream->new(MARKDOWN ? Text::Markdown::markdown($_[1]) : $_[1]);
87             }
88              
89             sub _render_partial_spec {
90 14     14   54 my ($self, $c, $path, $custom) = @_;
91 14         101 my $method = $c->param('method');
92 14   66     3579 my $validator = $custom || Mojolicious::Plugin::OpenAPI::_self($c)->validator;
93              
94 14 100       211 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     1998 %{$validator->bundle({schema => $schema})->data},
  13   100     2482  
103             $method ? (parameters => $validator->parameters_for_request([$method, $path])) : (),
104             }
105             );
106             }
107              
108             sub _render_spec {
109 46     46   187 my ($self, $c, $path, $custom) = @_;
110 46 100       256 return $self->_render_partial_spec($c, $path, $custom) if $path;
111              
112 32 100 100     383 my $openapi = $custom || $self->{standalone} ? undef : Mojolicious::Plugin::OpenAPI::_self($c);
113 32   100     148 my $format = $c->stash('format') || 'json';
114 32         469 my $validator = $custom;
115              
116 32 100 100     215 if (!$validator and $openapi) {
117 29   66     202 $validator = $openapi->{bundled} ||= $openapi->validator->bundle;
118 29         440294 $validator->base_url($c->req->url->to_abs->path($c->url_for($validator->base_url->path)));
119             }
120              
121 32 100       45612 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   12425 my $path_item = $_;
126 58         271 $path_item->{op_spec} = $validator->get([paths => @$path_item{qw(path method)}]);
127 58   100     7594 $path_item->{operation_id} //= '';
128 58         179 $path_item;
129 31         186 });
130              
131 31 100       353 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         538 operations => [sort { $a->{operation_id} cmp $b->{operation_id} } @$operations],
137             serialize => \&_serialize,
138             slugify => sub {
139 42     42   89381 join '-', map { s/\W/-/g; lc } map {"$_"} @_;
  126         354  
  126         425  
  126         296  
140             },
141 5         29 spec => $validator->data,
142             );
143             }
144              
145 66     66   75324 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__