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   20259 use Mojo::Base 'Mojolicious::Plugin';
  49         110  
  49         331  
3              
4 49     49   8738 use JSON::Validator;
  49         28898  
  49         330  
5 49     49   1219 use Mojo::JSON;
  49         106  
  49         2497  
6 49     49   288 use Scalar::Util qw(blessed);
  49         96  
  49         3047  
7              
8 49   50 49   347 use constant DEBUG => $ENV{MOJO_OPENAPI_DEBUG} || 0;
  49         113  
  49         3691  
9 49     49   293 use constant MARKDOWN => eval 'require Text::Markdown;1';
  49         117  
  49         4543  
10              
11             sub register {
12 60     60 1 845 my ($self, $app, $config) = @_;
13              
14 60         342 $app->defaults(openapi_spec_renderer_logo => '/mojolicious/plugin/openapi/logo.png');
15 60         1393 $app->defaults(openapi_spec_renderer_theme_color => '#508a25');
16              
17 60 100       1165 $self->{standalone} = $config->{openapi} ? 0 : 1;
18 60     34   447 $app->helper('openapi.render_spec' => sub { $self->_render_spec(@_) });
  34         174949  
19 60         33070 $app->helper('openapi.rich_text' => \&_helper_rich_text);
20              
21             # EXPERIMENTAL
22 60         30656 $app->helper('openapi.spec_iterator' => \&_helper_iterator);
23              
24 60 100       31689 unless ($app->{'openapi.render_specification'}++) {
25 52         132 push @{$app->renderer->classes}, __PACKAGE__;
  52         205  
26 52         576 push @{$app->static->classes}, __PACKAGE__;
  52         346  
27             }
28              
29 60 100       1082 $self->_register_with_openapi($app, $config) unless $self->{standalone};
30             }
31              
32             sub _helper_iterator {
33 108     108   47862 my ($c, $obj) = @_;
34 108 50       233 return unless $obj;
35              
36 108 100       283 unless ($c->{_helper_iterator}{$obj}) {
37 29         95 my $x_re = qr{^x-};
38             $c->{_helper_iterator}{$obj}
39 29         162 = [map { [$_, $obj->{$_}] } sort { lc $a cmp lc $b } grep { !/$x_re/ } keys %$obj];
  79         215  
  103         175  
  79         313  
40             }
41              
42 108         202 my $items = $c->{_helper_iterator}{$obj};
43 108         171 my $item = shift @$items;
44 108 100       279 delete $c->{_helper_iterator}{$obj} unless $item;
45 108 100       413 return $item ? @$item : ();
46             }
47              
48             sub _register_with_openapi {
49 59     59   174 my ($self, $app, $config) = @_;
50 59         147 my $openapi = $config->{openapi};
51              
52 59 50 50     390 if ($config->{render_specification} // 1) {
53             my $spec_route = $openapi->route->get(
54             '/',
55             [format => [qw(html json)]],
56             {format => undef},
57 27     27   405954 sub { shift->openapi->render_spec(@_) }
58 59         262 );
59 59   100     16562 my $name = $config->{spec_route_name} || $openapi->validator->get('/x-mojo-name');
60 59 100       5447 $spec_route->name($name) if $name;
61             }
62              
63 59 50 50     511 if ($config->{render_specification_for_paths} // 1) {
64 59     57   260 $app->plugins->once(openapi_routes_added => sub { $self->_add_documentation_routes(@_) });
  57         3290  
65             }
66             }
67              
68             sub _add_documentation_routes {
69 57     57   171 my ($self, $openapi, $routes) = @_;
70 57         109 my %dups;
71              
72 57         142 for my $route (@$routes) {
73 124         384 my $route_path = $route->to_string;
74 124 100       4909 next if $dups{$route_path}++;
75              
76 112         314 my $openapi_path = $route->to->{'openapi.path'};
77 112         1250 my $doc_route
78             = $openapi->route->options($route->pattern->unparsed, {'openapi.default_options' => 1});
79 112     12   32626 $doc_route->to(cb => sub { $self->_render_spec(shift, $openapi_path) });
  12         190360  
80 112 50       2486 $doc_route->name(join '_', $route->name, 'openapi_documentation') if $route->name;
81 112         2138 warn "[OpenAPI] Add route options $route_path (@{[$doc_route->name // '']})\n" if DEBUG;
82             }
83             }
84              
85             sub _helper_rich_text {
86 63     63   20586 return Mojo::ByteStream->new(MARKDOWN ? Text::Markdown::markdown($_[1]) : $_[1]);
87             }
88              
89             sub _render_partial_spec {
90 14     14   46 my ($self, $c, $path, $custom) = @_;
91 14         68 my $method = $c->param('method');
92 14   66     3438 my $validator = $custom || Mojolicious::Plugin::OpenAPI::_self($c)->validator;
93              
94 14 100       173 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     2217 %{$validator->bundle({schema => $schema})->data},
  13   100     2099  
103             $method ? (parameters => $validator->parameters_for_request([$method, $path])) : (),
104             }
105             );
106             }
107              
108             sub _render_spec {
109 46     46   138 my ($self, $c, $path, $custom) = @_;
110 46 100       236 return $self->_render_partial_spec($c, $path, $custom) if $path;
111              
112 32 100 100     338 my $openapi = $custom || $self->{standalone} ? undef : Mojolicious::Plugin::OpenAPI::_self($c);
113 32   100     102 my $format = $c->stash('format') || 'json';
114 32         340 my $validator = $custom;
115              
116 32 100 100     200 if (!$validator and $openapi) {
117 29   66     182 $validator = $openapi->{bundled} ||= $openapi->validator->bundle;
118 29         368886 $validator->base_url($c->req->url->to_abs->path($c->url_for($validator->base_url->path)));
119             }
120              
121 32 100       37342 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   9595 my $path_item = $_;
126 58         220 $path_item->{op_spec} = $validator->get([paths => @$path_item{qw(path method)}]);
127 58   100     5950 $path_item->{operation_id} //= '';
128 58         131 $path_item;
129 31         144 });
130              
131 31 100       309 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 8         418 operations => [sort { $a->{operation_id} cmp $b->{operation_id} } @$operations],
137             serialize => \&_serialize,
138             slugify => sub {
139 42     42   71257 join '-', map { s/\W/-/g; lc } map {"$_"} @_;
  126         275  
  126         342  
  126         234  
140             },
141 5         22 spec => $validator->data,
142             );
143             }
144              
145 66     66   60692 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__