Permalink
Cannot retrieve contributors at this time
| package Mojolicious::Plugin::OpenAPI::SpecRenderer; | |
| use Mojo::Base 'Mojolicious::Plugin'; | |
| use JSON::Validator; | |
| use Mojo::JSON; | |
| use Mojo::Util 'deprecated'; | |
| use constant DEBUG => $ENV{MOJO_OPENAPI_DEBUG} || 0; | |
| use constant MARKDOWN => eval 'require Text::Markdown;1'; | |
| sub register { | |
| my ($self, $app, $config) = @_; | |
| $app->defaults(openapi_spec_renderer_logo => '/mojolicious/plugin/openapi/logo.png'); | |
| $app->defaults(openapi_spec_renderer_theme_color => '#508a25'); | |
| $self->{standalone} = $config->{openapi} ? 0 : 1; | |
| $app->helper('openapi.render_spec' => sub { $self->_render_spec(@_) }); | |
| $app->helper('openapi.rich_text' => \&_helper_rich_text); | |
| # EXPERIMENTAL | |
| $app->helper('openapi.spec_iterator' => \&_helper_iterator); | |
| unless ($app->{'openapi.render_specification'}++) { | |
| push @{$app->renderer->classes}, __PACKAGE__; | |
| push @{$app->static->classes}, __PACKAGE__; | |
| } | |
| $self->_register_with_openapi($app, $config) unless $self->{standalone}; | |
| } | |
| sub _helper_iterator { | |
| my ($c, $obj) = @_; | |
| return unless $obj; | |
| unless ($c->{_helper_iterator}{$obj}) { | |
| my $x_re = qr{^x-}; | |
| $c->{_helper_iterator}{$obj} | |
| = [map { [$_, $obj->{$_}] } sort { lc $a cmp lc $b } grep { !/$x_re/ } keys %$obj]; | |
| } | |
| my $items = $c->{_helper_iterator}{$obj}; | |
| my $item = shift @$items; | |
| delete $c->{_helper_iterator}{$obj} unless $item; | |
| return $item ? @$item : (); | |
| } | |
| sub _register_with_openapi { | |
| my ($self, $app, $config) = @_; | |
| my $openapi = $config->{openapi}; | |
| if ($config->{render_specification} // 1) { | |
| my $spec_route = $openapi->route->get('/')->to(cb => sub { shift->openapi->render_spec(@_) }); | |
| my $name = $config->{spec_route_name} || $openapi->validator->get('/x-mojo-name'); | |
| $spec_route->name($name) if $name; | |
| } | |
| if ($config->{render_specification_for_paths} // 1) { | |
| $app->plugins->once(openapi_routes_added => sub { $self->_add_documentation_routes(@_) }); | |
| } | |
| } | |
| sub _add_documentation_routes { | |
| my ($self, $openapi, $routes) = @_; | |
| my %dups; | |
| for my $route (@$routes) { | |
| my $route_path = $route->to_string; | |
| next if $dups{$route_path}++; | |
| my $openapi_path = $route->to->{'openapi.path'}; | |
| my $doc_route | |
| = $openapi->route->options($route->pattern->unparsed, {'openapi.default_options' => 1}); | |
| $doc_route->to(cb => sub { $self->_render_spec(shift, $openapi_path) }); | |
| $doc_route->name(join '_', $route->name, 'openapi_documentation') if $route->name; | |
| warn "[OpenAPI] Add route options $route_path (@{[$doc_route->name // '']})\n" if DEBUG; | |
| } | |
| } | |
| sub _helper_rich_text { | |
| return Mojo::ByteStream->new(MARKDOWN ? Text::Markdown::markdown($_[1]) : $_[1]); | |
| } | |
| sub _render_partial_spec { | |
| my ($self, $c, $path, $custom_spec) = @_; | |
| my $validator | |
| = $custom_spec ? JSON::Validator->new->schema($custom_spec) | |
| : $self->{standalone} ? JSON::Validator->new->schema($c->stash('openapi_spec')) | |
| : Mojolicious::Plugin::OpenAPI::_self($c)->validator; | |
| my $method = $c->param('method'); | |
| my $bundled = $validator->get([paths => $path]); | |
| $bundled = $validator->bundle({schema => $bundled}) if $bundled; | |
| my $definitions = $bundled->{definitions} || {} if $bundled; | |
| my $parameters = $bundled->{parameters} || []; | |
| if ($method and $bundled = $bundled->{$method}) { | |
| push @$parameters, @{$bundled->{parameters} || []}; | |
| } | |
| return $c->render(json => {errors => [{message => 'No spec defined.'}]}, status => 404) | |
| unless $bundled; | |
| delete $bundled->{$_} for qw(definitions parameters); | |
| return $c->render( | |
| json => { | |
| '$schema' => 'http://json-schema.org/draft-04/schema#', | |
| title => $validator->get([qw(info title)]) || '', | |
| description => $validator->get([qw(info description)]) || '', | |
| definitions => $definitions, | |
| parameters => $parameters, | |
| %$bundled, | |
| } | |
| ); | |
| } | |
| sub _render_spec { | |
| my ($self, $c, $path, $custom_spec) = @_; | |
| deprecated '"openapi_spec" in stash is DEPRECATED' if $c->stash('openapi_spec'); | |
| return $self->_render_partial_spec($c, $path, $custom_spec) if $path; | |
| my $openapi | |
| = $custom_spec || $self->{standalone} ? undef : Mojolicious::Plugin::OpenAPI::_self($c); | |
| my $format = $c->stash('format') || 'json'; | |
| my %spec; | |
| if ($custom_spec) { | |
| %spec = %$custom_spec; | |
| } | |
| elsif ($openapi) { | |
| my $req_url = $c->req->url->to_abs; | |
| $openapi->{bundled} ||= $openapi->validator->bundle; | |
| %spec = %{$openapi->{bundled}}; | |
| if ($openapi->validator->version ge '3') { | |
| $spec{servers}[0]{url} = $req_url->to_string; | |
| $spec{servers}[0]{url} =~ s!\.(html|json)$!!; | |
| delete $spec{basePath}; # Added by Plugin::OpenAPI | |
| } | |
| else { | |
| $spec{basePath} = $c->url_for($spec{basePath}); | |
| $spec{host} = $req_url->host_port; | |
| $spec{schemes}[0] = $req_url->scheme; | |
| } | |
| } | |
| elsif ($c->stash('openapi_spec')) { | |
| %spec = %{$c->stash('openapi_spec') || {}}; | |
| } | |
| return $c->render(json => {errors => [{message => 'No specification to render.'}]}, status => 500) | |
| unless %spec; | |
| my ($x_re, $base_url, @operations) = (qr{^x-}); | |
| if ($format eq 'html') { | |
| for my $path (keys %{$spec{paths}}) { | |
| next if $path =~ $x_re; | |
| my $path_spec = $openapi ? $openapi->validator->get([paths => $path]) : $spec{paths}{$path}; | |
| for my $method (keys %$path_spec) { | |
| next if $method =~ $x_re; | |
| my $op_spec = $path_spec->{$method}; | |
| next unless ref $op_spec eq 'HASH'; | |
| push @operations, | |
| { | |
| method => $method, | |
| name => $op_spec->{operationId} ? $op_spec->{operationId} : join(' ', $method, $path), | |
| path => $path, | |
| op_spec => $op_spec, | |
| }; | |
| } | |
| } | |
| $base_url | |
| = exists $spec{openapi} | |
| ? Mojo::URL->new($spec{servers}[0]{url}) | |
| : Mojo::URL->new->host($spec{host} || 'localhost')->path($spec{basePath}) | |
| ->scheme($spec{schemes}[0]); | |
| } | |
| return $c->render(json => \%spec) unless $format eq 'html'; | |
| return $c->render( | |
| base_url => $base_url, | |
| handler => 'ep', | |
| template => 'mojolicious/plugin/openapi/layout', | |
| operations => [sort { $a->{name} cmp $b->{name} } @operations], | |
| serialize => \&_serialize, | |
| slugify => sub { | |
| join '-', map { s/\W/-/g; lc } map {"$_"} @_; | |
| }, | |
| spec => \%spec, | |
| ); | |
| } | |
| sub _serialize { Mojo::JSON::encode_json(@_) } | |
| 1; | |
| =encoding utf8 | |
| =head1 NAME | |
| Mojolicious::Plugin::OpenAPI::SpecRenderer - Render OpenAPI specification | |
| =head1 SYNOPSIS | |
| =head2 With Mojolicious::Plugin::OpenAPI | |
| $app->plugin(OpenAPI => { | |
| plugins => [qw(+SpecRenderer)], | |
| render_specification => 1, | |
| render_specification_for_paths => 1, | |
| %openapi_parameters, | |
| }); | |
| See L<Mojolicious::Plugin::OpenAPI/register> for what | |
| C<%openapi_parameters> might contain. | |
| =head2 Standalone | |
| use Mojolicious::Lite; | |
| plugin "Mojolicious::Plugin::OpenAPI::SpecRenderer"; | |
| # Some specification to render | |
| my $petstore = app->home->child("petstore.json"); | |
| get "/my-spec" => sub { | |
| my $c = shift; | |
| my $path = $c->param('path') || '/'; | |
| state $custom_spec = JSON::Validator->new->schema($petstore->to_string)->bundle; | |
| $c->openapi->render_spec($path, $custom_spec); | |
| }; | |
| =head1 DESCRIPTION | |
| L<Mojolicious::Plugin::OpenAPI::SpecRenderer> will enable | |
| L<Mojolicious::Plugin::OpenAPI> to render the specification in both HTML and | |
| JSON format. It can also be used L</Standalone> if you just want to render | |
| the specification, and not add any API routes to your application. | |
| See L</TEMPLATING> to see how you can override parts of the rendering. | |
| The human readable format focus on making the documentation printable, so you | |
| can easily share it with third parties as a PDF. If this documentation format | |
| is too basic or has missing information, then please | |
| L<report in|https://github.com/jhthorsen/mojolicious-plugin-openapi/issues> | |
| suggestions for enhancements. | |
| See L<https://demo.convos.by/api.html> for a demo. | |
| =head1 HELPERS | |
| =head2 openapi.render_spec | |
| $c = $c->openapi->render_spec; | |
| $c = $c->openapi->render_spec($json_path); | |
| $c = $c->openapi->render_spec($json_path, \%custom_spec); | |
| $c = $c->openapi->render_spec("/user/{id}"); | |
| Used to render the specification as either "html" or "json". Set the | |
| L<Mojolicious/stash> variable "format" to change the format to render. | |
| Will render the whole specification by default, but can also render | |
| documentation for a given OpenAPI path. | |
| =head2 openapi.rich_text | |
| $bytestream = $c->openapi->rich_text($text); | |
| Used to render the "description" in the specification with L<Text::Markdown> if | |
| it is installed. Will just return the text if the module is not available. | |
| =head1 METHODS | |
| =head2 register | |
| $doc->register($app, $openapi, \%config); | |
| Adds the features mentioned in the L</DESCRIPTION>. | |
| C<%config> is the same as passed on to | |
| L<Mojolicious::Plugin::OpenAPI/register>. The following keys are used by this | |
| plugin: | |
| =head3 render_specification | |
| Render the whole specification as either HTML or JSON from "/:basePath". | |
| Example if C<basePath> in your specification is "/api": | |
| GET https://api.example.com/api.html | |
| GET https://api.example.com/api.json | |
| Disable this feature by setting C<render_specification> to C<0>. | |
| =head3 render_specification_for_paths | |
| Render the specification from individual routes, using the OPTIONS HTTP method. | |
| Example: | |
| OPTIONS https://api.example.com/api/some/path.json | |
| OPTIONS https://api.example.com/api/some/path.json?method=post | |
| Disable this feature by setting C<render_specification_for_paths> to C<0>. | |
| =head1 TEMPLATING | |
| Overriding templates is EXPERIMENTAL, but not very likely to break in a bad | |
| way. | |
| L<Mojolicious::Plugin::OpenAPI::SpecRenderer> uses many template files to make | |
| up the human readable version of the spec. Each of them can be overridden by | |
| creating a file in your templates folder. | |
| mojolicious/plugin/openapi/layout.html.ep | |
| |- mojolicious/plugin/openapi/head.html.ep | |
| | '- mojolicious/plugin/openapi/style.html.ep | |
| |- mojolicious/plugin/openapi/header.html.ep | |
| | |- mojolicious/plugin/openapi/logo.html.ep | |
| | '- mojolicious/plugin/openapi/toc.html.ep | |
| |- mojolicious/plugin/openapi/intro.html.ep | |
| |- mojolicious/plugin/openapi/resources.html.ep | |
| | '- mojolicious/plugin/openapi/resource.html.ep | |
| | |- mojolicious/plugin/openapi/human.html.ep | |
| | |- mojolicious/plugin/openapi/parameters.html.ep | |
| | '- mojolicious/plugin/openapi/response.html.ep | |
| | '- mojolicious/plugin/openapi/human.html.ep | |
| |- mojolicious/plugin/openapi/references.html.ep | |
| |- mojolicious/plugin/openapi/footer.html.ep | |
| |- mojolicious/plugin/openapi/javascript.html.ep | |
| '- mojolicious/plugin/openapi/foot.html.ep | |
| See the DATA section in the source code for more details on styling and markup | |
| structure. | |
| L<https://github.com/jhthorsen/mojolicious-plugin-openapi/blob/master/lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm> | |
| Variables available in the templates: | |
| %= $serialize->($data_structure) | |
| %= $slugify->(@str) | |
| %= $spec->{info}{title} | |
| In addition, there is a logo in "header.html.ep" that can be overriden by | |
| either changing the static file "mojolicious/plugin/openapi/logo.png" or set | |
| "openapi_spec_renderer_logo" in L<stash|Mojolicious::Controller/stash> to a | |
| custom URL. | |
| =head1 SEE ALSO | |
| L<Mojolicious::Plugin::OpenAPI> | |
| =cut | |
| __DATA__ | |
| @@ mojolicious/plugin/openapi/header.html.ep | |
| <header class="openapi-header"> | |
| <h1 id="title"><%= $spec->{info}{title} || 'No title' %></h1> | |
| <p class="version"><span>Version</span> <span class="version"><%= $spec->{info}{version} %> - OpenAPI <%= $spec->{swagger} || $spec->{openapi} %></span></p> | |
| </header> | |
| <nav class="openapi-nav"> | |
| <a href="#top" class="openapi-logo"> | |
| %= image stash('openapi_spec_renderer_logo'), alt => 'OpenAPI Logo' | |
| </a> | |
| %= include 'mojolicious/plugin/openapi/toc' | |
| </nav> | |
| @@ mojolicious/plugin/openapi/intro.html.ep | |
| <h2 id="about"><a href="#about">About</a></h2> | |
| % if ($spec->{info}{description}) { | |
| <div class="description"> | |
| %== $c->openapi->rich_text($spec->{info}{description}) | |
| </div> | |
| % } | |
| % my $contact = $spec->{info}{contact}; | |
| % my $license = $spec->{info}{license}; | |
| <h3 id="license"><a href="#license">License</a></h3> | |
| % if ($license->{name}) { | |
| <p class="license"><a href="<%= $license->{url} || '' %>"><%= $license->{name} %></a></p> | |
| % } else { | |
| <p class="no-license">No license specified.</p> | |
| % } | |
| <h3 id="contact"><a href="#contact">Contact information</a></h3> | |
| % if ($contact->{email}) { | |
| <p class="contact-email"><a href="mailto:<%= $contact->{email} %>"><%= $contact->{email} %></a></p> | |
| % } | |
| % if ($contact->{url}) { | |
| <p class="contact-url"><a href="<%= $contact->{url} %>"><%= $contact->{url} %></a></p> | |
| % } | |
| % if (exists $spec->{openapi}) { | |
| <h3 id="servers"><a href="#servers">Servers</a></h3> | |
| <ul class="unstyled"> | |
| % for my $server (@{$spec->{servers}}){ | |
| <li><a href="<%= $server->{url} %>"><%= $server->{url} %></a><%= $server->{description} ? ' - '.$server->{description} : '' %></li> | |
| % } | |
| </ul> | |
| % } else { | |
| % my $schemes = $spec->{schemes} || ["http"]; | |
| % my $url = Mojo::URL->new("http://$spec->{host}"); | |
| <h3 id="baseurl"><a href="#baseurl">Base URL</a></h3> | |
| <ul class="unstyled"> | |
| % for my $scheme (@$schemes) { | |
| % $url->scheme($scheme); | |
| <li><a href="<%= $url %>"><%= $url %></a></li> | |
| % } | |
| </ul> | |
| % } | |
| % if ($spec->{info}{termsOfService}) { | |
| <h3 id="terms-of-service"><a href="#terms-of-service">Terms of service</a></h3> | |
| <p class="terms-of-service"> | |
| %= $spec->{info}{termsOfService} | |
| </p> | |
| % } | |
| @@ mojolicious/plugin/openapi/foot.html.ep | |
| <a href="#top" class="openapi-up-button" type="button">⌃</a> | |
| <script> | |
| new SpecRenderer().setup(); | |
| </script> | |
| @@ mojolicious/plugin/openapi/footer.html.ep | |
| <!-- default footer --> | |
| @@ mojolicious/plugin/openapi/head.html.ep | |
| <title><%= $spec->{info}{title} || 'No title' %></title> | |
| <meta charset="utf-8"> | |
| <meta http-equiv="X-UA-Compatible" content="chrome=1"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=2.0"> | |
| %= include 'mojolicious/plugin/openapi/style' | |
| @@ mojolicious/plugin/openapi/human.html.ep | |
| % if ($op_spec->{summary}) { | |
| <p class="spec-summary"><%= $op_spec->{summary} %></p> | |
| % } | |
| % if ($op_spec->{description}) { | |
| <div class="spec-description"><%== $c->openapi->rich_text($op_spec->{description}) %></div> | |
| % } | |
| % if (!$op_spec->{description} and !$op_spec->{summary}) { | |
| <p class="op-summary op-doc-missing">This resource is not documented.</p> | |
| % } | |
| @@ mojolicious/plugin/openapi/parameters.html.ep | |
| % my $has_parameters = @{$op_spec->{parameters} || []}; | |
| % my $body; | |
| <h4 class="op-parameters">Parameters</h3> | |
| % if ($has_parameters) { | |
| <table class="op-parameters"> | |
| <thead> | |
| <tr> | |
| <th>Name</th> | |
| <th>In</th> | |
| <th>Type</th> | |
| <th>Required</th> | |
| <th>Description</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| % } | |
| % for my $p (@{$op_spec->{parameters} || []}) { | |
| % $body = $p->{schema} if $p->{in} eq 'body'; | |
| <tr> | |
| % if ($spec->{parameters}{$p->{name}}) { | |
| <td><a href="#<%= $slugify->(qw(ref parameters), $p->{name}) %>"><%= $p->{name} %></a></td> | |
| % } else { | |
| <td><%= $p->{name} %></td> | |
| % } | |
| <td><%= $p->{in} %></td> | |
| <td><%= $p->{type} || $p->{schema}{type} %></td> | |
| <td><%= $p->{required} ? "Yes" : "No" %></td> | |
| <td><%== $p->{description} ? $c->openapi->rich_text($p->{description}) : "" %></td> | |
| </tr> | |
| % } | |
| % if ($has_parameters) { | |
| </tbody> | |
| </table> | |
| % } else { | |
| <p class="op-parameters">This resource has no input parameters.</p> | |
| % } | |
| % if ($body) { | |
| <h4 class="op-parameter-body">Body</h4> | |
| <pre class="op-parameter-body"><%= $serialize->($body) %></pre> | |
| % } | |
| % if ($op_spec->{requestBody}) { | |
| <h4 class="op-parameter-body">requestBody</h4> | |
| <pre class="op-parameter-body"><%= $serialize->($op_spec->{requestBody}{content}) %></pre> | |
| % } | |
| @@ mojolicious/plugin/openapi/response.html.ep | |
| % while (my ($code, $res) = $c->openapi->spec_iterator($op_spec->{responses})) { | |
| <h4 class="op-response">Response <%= $code %></h3> | |
| %= include 'mojolicious/plugin/openapi/human', op_spec => $res | |
| <pre class="op-response"><%= $serialize->($res->{schema} || $res->{content}) %></pre> | |
| % } | |
| @@ mojolicious/plugin/openapi/resource.html.ep | |
| % my $id = $slugify->(op => $method, $path); | |
| <h3 id="<%= $id %>" class="op-path <%= $op_spec->{deprecated} ? "deprecated" : "" %>"> | |
| <a href="#<%= $id %>"><%= $name %></a> | |
| </h3> | |
| % if ($op_spec->{deprecated}) { | |
| <p class="op-deprecated">This resource is deprecated!</p> | |
| % } | |
| <ul class="unstyled"> | |
| <li><b><%= uc $method %></b> <a href="<%= "$base_url$path" %>"><%= $base_url->path . $path %></a></li> | |
| % if ($op_spec->{operationId}) { | |
| <li><b>Operation ID:</b> <span><%= $op_spec->{operationId} %></span></li> | |
| % } | |
| </ul> | |
| %= include 'mojolicious/plugin/openapi/human', op_spec => $op_spec | |
| %= include 'mojolicious/plugin/openapi/parameters', op_spec => $op_spec | |
| %= include 'mojolicious/plugin/openapi/response', op_spec => $op_spec | |
| @@ mojolicious/plugin/openapi/references.html.ep | |
| % if ($spec->{parameters}) { | |
| <h2 id="parameters"><a href="#parameters">Parameters</a></h2> | |
| % while (my ($key, $schema) = $c->openapi->spec_iterator($spec->{parameters})) { | |
| % my $id = lc $slugify->(qw(ref parameters), $key); | |
| <h3 id="<%= $id %>"><a href="#<%= $id %>"><%= $key %></a></h3> | |
| <pre class="ref"><%= $serialize->($schema) %></pre> | |
| % } | |
| </li> | |
| % } | |
| % if ($spec->{components}) { | |
| <h2 id="components"><a href="#components">Components</a></h2> | |
| % while (my ($type, $comp_group) = $c->openapi->spec_iterator($spec->{components})) { | |
| % while (my ($key, $comp) = $c->openapi->spec_iterator($comp_group)) { | |
| <li><a href="#<%= lc $slugify->(qw(ref components), $key) %>"><%= $key %></a></li> | |
| % } | |
| % } | |
| % } | |
| % if ($spec->{definitions}) { | |
| <h2 id="definitions"><a href="#definitions">Definitions</a></h2> | |
| % while (my ($key, $schema) = $c->openapi->spec_iterator($spec->{definitions})) { | |
| % my $id = lc $slugify->(qw(ref definitions), $key); | |
| <h3 id="<%= $id %>"><a href="#<%= $id %>"><%= $key %></a></h3> | |
| <pre class="ref"><%= $serialize->($schema) %></pre> | |
| % } | |
| </li> | |
| % } | |
| @@ mojolicious/plugin/openapi/resources.html.ep | |
| <h2 id="resources"><a href="#resources">Resources</a></h2> | |
| % for my $op (@$operations) { | |
| %= include 'mojolicious/plugin/openapi/resource', %$op; | |
| % } | |
| @@ mojolicious/plugin/openapi/toc.html.ep | |
| <ol id="toc"> | |
| % if ($spec->{info}{description}) { | |
| <li class="for-description"> | |
| <a href="#about">About</a> | |
| <ol> | |
| <li><a href="#license">License</a></li> | |
| <li><a href="#contact">Contact</a></li> | |
| <li><a href="#baseurl">Base URL</a></li> | |
| % if ($spec->{info}{termsOfService}) { | |
| <li class="for-terms"><a href="#terms-of-service">Terms of service</a></li> | |
| % } | |
| </ol> | |
| </li> | |
| % } | |
| <li class="for-resources"> | |
| <a href="#resources">Resources</a> | |
| <ol> | |
| % for my $op (@$operations) { | |
| <li><a href="#<%= $slugify->(op => @$op{qw(method path)}) %>"><%= $op->{name} %></a></li> | |
| % } | |
| </ol> | |
| </li> | |
| % if ($spec->{parameters}) { | |
| <li class="for-references for-parameters"> | |
| <a href="#references">Parameters</a> | |
| <ol> | |
| % while (my ($key) = $c->openapi->spec_iterator($spec->{parameters})) { | |
| <li><a href="#<%= lc $slugify->(qw(ref parameters), $key) %>"><%= $key %></a></li> | |
| % } | |
| </ol> | |
| </li> | |
| % } | |
| % if ($spec->{components}) { | |
| <li class="for-references for-components"> | |
| <a href="#references">Components</a> | |
| <ol> | |
| % while (my ($type, $comp_group) = $c->openapi->spec_iterator($spec->{components})) { | |
| % while (my ($key, $comp) = $c->openapi->spec_iterator($comp_group)) { | |
| <li><a href="#<%= lc $slugify->(qw(ref components), $key) %>"><%= $key %></a></li> | |
| % } | |
| % } | |
| </ol> | |
| </li> | |
| % } | |
| % if ($spec->{definitions}) { | |
| <li class="for-references for-definitions"> | |
| <a href="#references">Definitions</a> | |
| <ol> | |
| % while (my ($key) = $c->openapi->spec_iterator($spec->{definitions})) { | |
| <li><a href="#<%= lc $slugify->(qw(ref definitions), $key) %>"><%= $key %></a></li> | |
| % } | |
| </ol> | |
| </li> | |
| % } | |
| </ol> | |
| @@ mojolicious/plugin/openapi/layout.html.ep | |
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| %= include 'mojolicious/plugin/openapi/head' | |
| </head> | |
| <body> | |
| <div id="top" class="container openapi-container"> | |
| %= include 'mojolicious/plugin/openapi/header' | |
| <article class="openapi-spec"> | |
| <section class="openapi-spec_intro"> | |
| %= include 'mojolicious/plugin/openapi/intro' | |
| </section> | |
| <section class="openapi-spec_resources"> | |
| %= include 'mojolicious/plugin/openapi/resources' | |
| </section> | |
| <section class="openapi-spec_references"> | |
| %= include 'mojolicious/plugin/openapi/references' | |
| </section> | |
| </article> | |
| <footer class="openapi-footer"> | |
| %= include 'mojolicious/plugin/openapi/footer' | |
| </footer> | |
| </div> | |
| %= include "mojolicious/plugin/openapi/javascript" | |
| %= include "mojolicious/plugin/openapi/foot" | |
| </body> | |
| </html> | |
| @@ mojolicious/plugin/openapi/javascript.html.ep | |
| <script> | |
| var SpecRenderer = function() {}; | |
| function findVisibleElements(containerEl) { | |
| var els = [].slice.call(containerEl.childNodes, 0); | |
| var haystack = []; | |
| // Filter out comments, text nodes, ... | |
| var i = 0; | |
| while (i < els.length) { | |
| if (els[i].nodeType == Node.ELEMENT_NODE) { | |
| haystack.push([i, els[i]]); | |
| i++; | |
| } | |
| else { | |
| els.splice(i, 1); | |
| } | |
| } | |
| // No child nodes | |
| if (!els.length) return []; | |
| // Find fist visible element | |
| var scrollTop = document.documentElement.scrollTop || document.body.scrollTop; | |
| while (haystack.length > 1) { | |
| var i = Math.floor(haystack.length / 2); | |
| if (haystack[i][1].offsetTop <= scrollTop) { | |
| haystack.splice(0, i); | |
| } | |
| else { | |
| haystack.splice(i); | |
| } | |
| } | |
| if (!haystack.length) haystack.push([0, els[0]]); | |
| // Figure out the first and last visible element | |
| var offsetHeight = window.innerHeight; | |
| var firstIdx = haystack[0][0]; | |
| var lastIdx = firstIdx; | |
| while (lastIdx < els.length) { | |
| if (els[lastIdx].offsetTop > scrollTop + offsetHeight) break; | |
| lastIdx++; | |
| } | |
| return els.slice(firstIdx, lastIdx); | |
| } | |
| SpecRenderer.prototype.jsonhtmlify | |
| = function(e){let n=document.createElement('div');const t=[[e,n]],s=[];for(;t.length;){const[e,l]=t.shift();let a,c,o=typeof e;if(null===e||'undefined'==o?o='null':Array.isArray(e)&&(o='array'),'array'==o)(c=(e=>e)).len=e.length,(a=document.createElement('div')).className='json-array '+(c.len?'has-items':'is-empty');else if('object'==o){const n=Object.keys(e).sort();(c=(e=>n[e])).len=n.length,(a=document.createElement('div')).className='json-object '+(c.len?'has-items':'is-empty')}else(a=document.createElement('span')).className='json-'+o,a.textContent='null'==o?'null':'boolean'!=o?e:e?'true':'false';if(c){const i=document.createElement('span');if(i.className='json-type',i.textContent=c.len?o+'['+c.len+']':'{}',l.appendChild(i),-1!=s.indexOf(e))n.classList.add('has-recursive-items'),a.classList.add('is-seen');else{for(let n=0;n<c.len;n++){const s=c(n),l=document.createElement('div'),o=document.createElement('span');o.className='json-key',o.textContent=s,l.appendChild(o),a.appendChild(l),t.push([e[s],l])}s.push(e)}}l.className='json-item '+a.className.replace(/^json-/,'contains-'),l.appendChild(a)}return n} | |
| SpecRenderer.prototype.renderNav = function() { | |
| var i = 0; | |
| if (this.firstHeadingEl.offsetTop < this.scrollTop) { | |
| for (i = 0; i < this.headings.length; i++) { | |
| if (this.headings[i].offsetTop >= this.scrollTop + this.wh - this.headingOffsetTop) break; | |
| } | |
| } | |
| if (i > 0) i--; | |
| var id = this.headings[i] && this.headings[i].id || ''; | |
| var aEl = document.querySelector('.openapi-nav a[href$="#' + id + '"]'); | |
| for (i = 0; i < this.aEls.length; i++) { | |
| this.aEls[i].parentNode.classList[this.aEls[i] == aEl ? 'add' : 'remove']('is-active'); | |
| } | |
| }; | |
| SpecRenderer.prototype.renderPreTags = function() { | |
| var ki, pi; | |
| for (pi = 0; pi < this.visiblePreTags.length; pi++) { | |
| var preEl = this.visiblePreTags[pi]; | |
| var jsonEl = this.jsonhtmlify(JSON.parse(preEl.innerText)); | |
| jsonEl.classList.add('json-container'); | |
| preEl.parentNode.replaceChild(jsonEl, preEl); | |
| var keyEls = jsonEl.querySelectorAll('.json-key'); | |
| for (ki = 0; ki < keyEls.length; ki++) { | |
| if (keyEls[ki].textContent != '$ref') continue; | |
| var refEl = keyEls[ki].nextElementSibling; | |
| refEl.parentNode.replaceChild(this.renderRefLink(refEl), refEl); | |
| } | |
| } | |
| }; | |
| SpecRenderer.prototype.renderRefLink = function(refEl) { | |
| var a = document.createElement('a'); | |
| var href = refEl.textContent.replace(/'/g, ''); | |
| a.textContent = refEl.textContent; | |
| a.href = href.match(/^#/) ? '#ref-' + href.replace(/\W/g, '-').substring(2).toLowerCase() : href; | |
| return a; | |
| }; | |
| SpecRenderer.prototype.renderUpButton = function() { | |
| this.upButton.classList[this.scrollTop > 150 ? 'add' : 'remove']('is-visible'); | |
| }; | |
| SpecRenderer.prototype.render = function() { | |
| this.scrollTop = document.documentElement.scrollTop || document.body.scrollTop; | |
| var visiblePreTags = []; | |
| findVisibleElements(document.querySelector('.openapi-spec')).forEach(function(el) { | |
| findVisibleElements(el).forEach(function(el) { | |
| if (el.tagName.toLowerCase() == 'pre') visiblePreTags.push(el); | |
| }); | |
| }); | |
| this.visiblePreTags = visiblePreTags; | |
| this.renderNav(); | |
| this.renderPreTags(); | |
| this.renderUpButton(); | |
| }; | |
| SpecRenderer.prototype.scrollSpy = function(e) { | |
| // Do not run this method too often | |
| if (e && e.preventDefault) return this._scrollSpyTid || (this._scrollSpyTid = setTimeout(this.scrollSpy, 100)); | |
| if (this._scrollSpyTid) clearTimeout(this._scrollSpyTid); | |
| delete this._scrollSpyTid; | |
| this.wh = window.innerHeight; | |
| this.headingOffsetTop = parseInt(this.wh / 2.3, 10); | |
| this.render(); | |
| } | |
| SpecRenderer.prototype.setup = function() { | |
| this.aEls = document.querySelectorAll('.openapi-nav a'); | |
| this.firstHeadingEl = document.querySelector('h2'); | |
| this.headings = document.querySelectorAll('h3[id]'); | |
| this.upButton = document.querySelector('.openapi-up-button'); | |
| this.scrollSpy = this.scrollSpy.bind(this); | |
| this.render(); | |
| var self = this; | |
| ['click', 'resize', 'scroll'].forEach(function(name) { window.addEventListener(name, self.scrollSpy) }); | |
| }; | |
| </script> | |
| @@ mojolicious/plugin/openapi/style.html.ep | |
| <style> | |
| * { box-sizing: border-box; } | |
| html, body { | |
| background: #f7f7f7; | |
| font-family: 'Gotham Narrow SSm','Helvetica Neue',Helvetica,sans-serif; | |
| font-size: 16px; | |
| color: #222; | |
| line-height: 1.4em; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| padding: 1rem; | |
| } | |
| a { color: <%= $openapi_spec_renderer_theme_color %>; text-decoration: underline; word-break: break-word; } | |
| a:hover { text-decoration: none; } | |
| h1, h2, h3, h4 { font-family: Verdana; color: #403f41; font-weight: bold; line-height: 1.2em; margin: 1em 0; padding-top: 0.4rem; } | |
| h1 a, h2 a, h3 a, h4 a { text-decoration: none; color: inherit; } | |
| h1 a:hover, h2 a:hover, h3 a:hover, h4 a:hover { text-decoration: underline; } | |
| h1 { font-size: 2.4em; } | |
| h1 { margin-top: 0; padding-top: 1em; } | |
| h2 { font-size: 1.8em; border-bottom: 2px solid #cfd4c5; } | |
| h3 { font-size: 1.4em; } | |
| h4 { font-size: 1.1em; } | |
| table { | |
| margin: 0em -0.4rem; | |
| width: 100%; | |
| border-collapse: collapse; | |
| } | |
| td, th { | |
| vertical-align: top; | |
| text-align: left; | |
| padding: 0.4rem; | |
| } | |
| th { | |
| font-weight: bold; | |
| border-bottom: 1px solid #ccc; | |
| } | |
| td p, th p { | |
| margin: 0; | |
| } | |
| ol, | |
| ul { | |
| margin: 0; | |
| padding: 0 1.5em; | |
| } | |
| ul.unstyled { | |
| list-style: none; | |
| padding: 0; | |
| } | |
| p { | |
| margin: 1em 0; | |
| } | |
| .json-container, | |
| pre { | |
| background: #f1f3ed; | |
| font-size: 0.9rem; | |
| line-height: 1.4em; | |
| letter-spacing: -0.02em; | |
| border-left: 4px solid <%= $openapi_spec_renderer_theme_color %>; | |
| padding: 0.5em; | |
| margin: 1rem 0rem; | |
| overflow: auto; | |
| } | |
| .openapi-nav a { | |
| text-decoration: none; | |
| line-height: 1.5rem; | |
| white-space: nowrap; | |
| } | |
| .openapi-logo { display: none; } | |
| .openapi-nav ol { margin: 0.2rem 0 0.5rem 0; } | |
| .openapi-up-button { display: none; } | |
| .openapi-container { max-width: 50rem; margin: 0 auto; } | |
| p.version { margin: -1rem 0 2em 0; } | |
| p.op-deprecated { color: #c00; } | |
| h3.op-path { margin-top: 2em; padding: 0.5rem 0 0 0; } | |
| h2 + h3.op-path { margin-top: 1em; } | |
| .json-item .json-item { | |
| border: 0; | |
| padding: 0; | |
| margin: 0; | |
| margin-left: 0.4rem; | |
| padding-left: 0.4rem; | |
| } | |
| .json-array > .json-item > .json-key, | |
| .json-array > .json-item > .json-key + .json-type { display: none; } | |
| .json-array > .json-item > .json-string:before { content: '- '; color: #222; font-weight: bold; } | |
| .json-boolean, .json-number, .json-string { color: <%= $openapi_spec_renderer_theme_color %>; font-weight: 500; } | |
| .json-key:after { content: ': '; color: #222; } | |
| .json-null { color: #222; } | |
| .json-type { color: #c5a138; display: none; } | |
| .json-item:hover > .json-type { display: inline; } | |
| .json-container > .json-type { display: none !important; } | |
| .json-container > div > .json-item { padding: 0; margin: 0; } | |
| @media only screen { | |
| .openapi-up-button { | |
| background: <%= $openapi_spec_renderer_theme_color %>; | |
| color: #f2f3ed; | |
| font-weight: bold; | |
| font-size: 1.2rem; | |
| line-height: 1.5em; | |
| text-align: center; | |
| border: 0; | |
| box-shadow: 0 0 3px 3px rgba(0, 0, 0, 0.2); | |
| border-radius: 50%; | |
| padding-top: 0.3em; | |
| width: 2.1em; | |
| height: 2.1em; | |
| opacity: 0; | |
| position: fixed; | |
| bottom: 1.5rem; | |
| left: calc(50vw + 31rem); | |
| transition: background 0.25s ease-in-out, opacity 0.25s ease-in-out; | |
| cursor: pointer; | |
| } | |
| .openapi-up-button:hover { | |
| background: #2c520f; | |
| } | |
| .openapi-up-button.is-visible { | |
| opacity: 0.9; | |
| } | |
| } | |
| @media only screen and (max-width: 70rem) { | |
| .openapi-up-button { | |
| left: auto; | |
| right: 1rem; | |
| } | |
| } | |
| @media only screen and (min-width: 60rem) { | |
| body { | |
| padding: 0; | |
| } | |
| .openapi-up-button { | |
| display: block; | |
| } | |
| .openapi-container { | |
| max-width: 70rem; | |
| } | |
| .openapi-nav { | |
| padding: 1.4rem 0 3rem 1rem; | |
| width: 18rem; | |
| height: 100vh; | |
| overflow: auto; | |
| -webkit-overflow-scrolling: touch; | |
| position: fixed; | |
| top: 0; | |
| } | |
| .openapi-logo { | |
| display: block; | |
| margin-bottom: 1rem; | |
| } | |
| .openapi-logo img { | |
| max-height: 100px; | |
| max-width 100%; | |
| } | |
| .openapi-nav ol { | |
| list-style: none; | |
| padding: 0; | |
| margin: 0; | |
| } | |
| .openapi-nav li a { | |
| margin: -0.1rem -0.2rem; | |
| padding: 0.1rem 0.4rem; | |
| display: block; | |
| } | |
| .openapi-nav li a:hover { | |
| background: #e5e8df; | |
| text-decoration: none; | |
| } | |
| .openapi-nav > ol > li > a { | |
| color: #403f41; | |
| font-weight: bold; | |
| font-size: 1.1rem; | |
| line-height: 1.8em; | |
| } | |
| .openapi-nav li.is-active a { | |
| background: #e3e6de; | |
| } | |
| .openapi-nav .method { | |
| font-size: 0.8rem; | |
| color: #222; | |
| width: auto; | |
| } | |
| .openapi-footer, | |
| .openapi-header, | |
| .openapi-spec { | |
| margin-left: 21rem; | |
| margin-right: 1rem; | |
| } | |
| .openapi-footer { | |
| border-top: 4px solid #cfd4c5; | |
| padding-top: 2.5rem; | |
| margin-top: 4rem; | |
| } | |
| #about { | |
| height: 1px; | |
| overflow: hidden; | |
| position: absolute; | |
| top: -10rem; | |
| opacity: 0; | |
| } | |
| } | |
| </style> | |
| @@ mojolicious/plugin/openapi/logo.png (base64) | |
| iVBORw0KGgoAAAANSUhEUgAAAMgAAAA5CAMAAABESJQQAAAABGdBTUEAALGPC/xhBQAAACBjSFJN | |
| AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAC+lBMVEVHcExXYExNTE5GREZH | |
| RkhLSUxZXFMAAElLSkxEQ0VDQkRDQkNDQkRDQkRFRUZMSk1LSkxJR0pRU09DQkREQ0VOT0tUVFFE | |
| Q0RHRkd8gHxNS05CQUNUVFF2tk95qlByqkt2rU5vp0hfZ09ZZkdLS0xDQkRCQUN2qU5wqUptp0Zt | |
| p0ZupkZspkRwqEhLXDJNXDNOXTNOXTROXTRSYDdYZz5nkVNEQ0VNTE9wqEpup0ZrpUNupEZQXzVM | |
| WzFSYThSYjhgYVyArlZwp0htpkZNXDFNXDNRYDdeZk1RUU1EQ0VDQkSi01KVyEhtp0VSYDdCQUNL | |
| SkuWyT+XykJ0q0xNXDJYXVBYWlVFREZFREZCQkOtx22YykWWyUCVyD2VyD6Wx0ltpkZMS05FREZJ | |
| SEmXykKWyUB8rE1NXDNFREZCQUNGRUdFREZfX19XZztOXTRth0KWyUGUyD2XykRRYjdOTU9PXTRw | |
| iUWVyD+Mu0xtp0dNXDJPXjVMS01PXjSWyUGYykN2qlBspkRRUFJbXlhPXjSUyT5PTlBRYDdYZz1N | |
| XDFspkRRYTdNXDFSXj1tg0CVyT6Zy0VaX1JEQ0VQXzZDQURSYjlxoU1wqElSYDdSYTmXykdSYThT | |
| YTlJSEpJR0lCQUNJSEpEREVRUVNKSEtYWFhPTlFOTlBNTE1GRkdISElHR0lxcHJxbXFEREVcXFVh | |
| YGJycXJ7e354eHxsa25JSUpFQ0Y/PkpBQUNIR0hAQEdIR0lKSUtHRkiJrmVEQUSenpyhs46Vm4Nv | |
| qElwqUpGRUdEQ0VIR0lKSEpHRkiWykGVyD5CQUNCQUNHRkdJSUqn1l6Vxklvp0dTU1VBQUVEQERE | |
| Q0VKSkyYy0hrpkNfX2JGRUdPT1B0pk9rpkRHRkhFREVJSEtEREVLSkyVyD+WyUB1pFBZWVqVxU91 | |
| olBup0hnZ2ptpkZDQ0NKSkuXyUN0plBMWzKXxkyXyERXV1lKSUtTYThPTVBKSktDQUNCQUNrpUNM | |
| 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 | |
| KzAwOjAw+374IAAAAABJRU5ErkJggg== |