Mojolicious-Plugin-OpenAPI-5.11/0000755000175100001660000000000014766301352015773 5ustar runnerdockerMojolicious-Plugin-OpenAPI-5.11/META.json0000644000175100001660000000563214766301352017422 0ustar runnerdocker{ "abstract" : "OpenAPI / Swagger plugin for Mojolicious", "author" : [ "Jan Henning Thorsen " ], "dynamic_config" : 0, "generated_by" : "ExtUtils::MakeMaker version 7.70, CPAN::Meta::Converter version 2.150010", "license" : [ "artistic_2" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : 2 }, "name" : "Mojolicious-Plugin-OpenAPI", "no_index" : { "directory" : [ "t", "inc", "examples", "t" ] }, "prereqs" : { "build" : { "requires" : { "ExtUtils::MakeMaker" : "0" } }, "configure" : { "requires" : { "ExtUtils::MakeMaker" : "0" } }, "runtime" : { "recommends" : { "Text::Markdown" : "v1.0.31" }, "requires" : { "JSON::Validator" : "5.13", "Mojolicious" : "9.00", "perl" : "5.016" } }, "test" : { "requires" : { "Test::More" : "0.88" } } }, "release_status" : "stable", "resources" : { "bugtracker" : { "web" : "https://github.com/jhthorsen/mojolicious-plugin-openapi/issues" }, "homepage" : "https://github.com/jhthorsen/mojolicious-plugin-openapi", "license" : [ "http://www.opensource.org/licenses/artistic-license-2.0" ], "repository" : { "type" : "git", "url" : "https://github.com/jhthorsen/mojolicious-plugin-openapi.git", "web" : "https://github.com/jhthorsen/mojolicious-plugin-openapi" }, "x_IRC" : { "url" : "irc://irc.libera.chat/#perl-openapi", "web" : "https://web.libera.chat/#perl-openapi" } }, "version" : "5.11", "x_contributors" : [ "Bernhard Graf ", "Doug Bell ", "Ed J ", "Henrik Andersen ", "Ilya Rassadin ", "Jan Henning Thorsen ", "Ji-Hyeon Gim ", "Joel Berger ", "Krasimir Berov ", "Lars Thegler ", "Lee Johnson ", "Linn-Hege Kristensen ", "Manuel ", "Martin Renvoize ", "Mohammad S Anwar ", "Nick Morrott ", "Renee ", "Roy Storey ", "SebMourlhou <35918953+SebMourlhou@users.noreply.github.com>", "Søren Lund ", "Stephan Hradek ", "Stephan Hradek " ], "x_serialization_backend" : "JSON::PP version 4.16" } Mojolicious-Plugin-OpenAPI-5.11/lib/0000755000175100001660000000000014766301352016541 5ustar runnerdockerMojolicious-Plugin-OpenAPI-5.11/lib/Mojolicious/0000755000175100001660000000000014766301352021035 5ustar runnerdockerMojolicious-Plugin-OpenAPI-5.11/lib/Mojolicious/Plugin/0000755000175100001660000000000014766301352022273 5ustar runnerdockerMojolicious-Plugin-OpenAPI-5.11/lib/Mojolicious/Plugin/OpenAPI.pm0000644000175100001660000005012114766301352024063 0ustar runnerdockerpackage Mojolicious::Plugin::OpenAPI; use Mojo::Base 'Mojolicious::Plugin'; use JSON::Validator; use Mojo::JSON; use Mojo::Util; use Mojolicious::Plugin::OpenAPI::Parameters; use constant DEBUG => $ENV{MOJO_OPENAPI_DEBUG} || 0; our $VERSION = '5.11'; has route => sub {undef}; has validator => sub { JSON::Validator::Schema->new; }; sub register { my ($self, $app, $config) = @_; $self->validator(JSON::Validator->new->schema($config->{url} || $config->{spec})->schema); $self->validator->coerce($config->{coerce}) if defined $config->{coerce}; if (my $class = $config->{version_from_class} // ref $app) { $self->validator->data->{info}{version} = sprintf '%s', $class->VERSION if $class->VERSION; } my $errors = $config->{skip_validating_specification} ? [] : $self->validator->errors; die @$errors if @$errors; unless ($app->defaults->{'openapi.base_paths'}) { $app->helper('openapi.spec' => \&_helper_get_spec); $app->helper('openapi.valid_input' => \&_helper_valid_input); $app->helper('openapi.validate' => \&_helper_validate); $app->helper('reply.openapi' => \&_helper_reply); $app->hook(before_render => \&_before_render); $app->renderer->add_handler(openapi => \&_render); } $self->{log_level} = $ENV{MOJO_OPENAPI_LOG_LEVEL} || $config->{log_level} || 'warn'; $self->_build_route($app, $config); # This plugin is required my @plugins = (Mojolicious::Plugin::OpenAPI::Parameters->new->register($app, $config)); for my $plugin (@{$config->{plugins} || [qw(+Cors +SpecRenderer +Security)]}) { $plugin = "Mojolicious::Plugin::OpenAPI::$plugin" if $plugin =~ s!^\+!!; eval "require $plugin;1" or Carp::confess("require $plugin: $@"); push @plugins, $plugin->new->register($app, {%$config, openapi => $self}); } my %default_response = %{$config->{default_response} || {}}; $default_response{name} ||= $config->{default_response_name} || 'DefaultResponse'; $default_response{status} ||= $config->{default_response_codes} || [400, 401, 404, 500, 501]; $default_response{location} = 'definitions'; $self->validator->add_default_response(\%default_response) if @{$default_response{status}}; $self->_add_routes($app, $config); return $self; } sub _add_routes { my ($self, $app, $config) = @_; my $op_spec_to_route = $config->{op_spec_to_route} || '_op_spec_to_route'; my (@routes, %uniq); for my $route ($self->validator->routes->each) { my $op_spec = $self->validator->get([paths => @$route{qw(path method)}]); my $name = $op_spec->{'x-mojo-name'} || $op_spec->{operationId}; my $r; die qq([OpenAPI] operationId "$op_spec->{operationId}" is not unique) if $op_spec->{operationId} and $uniq{o}{$op_spec->{operationId}}++; die qq([OpenAPI] Route name "$name" is not unique.) if $name and $uniq{r}{$name}++; if (!$op_spec->{'x-mojo-to'} and $name) { $r = $self->route->root->find($name); warn "[OpenAPI] Found existing route by name '$name'.\n" if DEBUG and $r; $self->route->add_child($r) if $r; } if (!$r) { my $http_method = $route->{method}; my $route_path = $self->_openapi_path_to_route_path(@$route{qw(method path)}); $name ||= $op_spec->{operationId}; warn "[OpenAPI] Creating new route for '$route_path'.\n" if DEBUG; $r = $self->route->$http_method($route_path); $r->name("$self->{route_prefix}$name") if $name; } $r->to(format => undef, 'openapi.method' => $route->{method}, 'openapi.path' => $route->{path}); $self->$op_spec_to_route($op_spec, $r, $config); warn "[OpenAPI] Add route $route->{method} @{[$r->to_string]} (@{[$r->name // '']})\n" if DEBUG; push @routes, $r; } $app->plugins->emit_hook(openapi_routes_added => $self, \@routes); } sub _before_render { my ($c, $args) = @_; return unless _self($c); my $handler = $args->{handler} || 'openapi'; # Call _render() for response data return if $handler eq 'openapi' and exists $c->stash->{openapi} or exists $args->{openapi}; # Fallback to default handler for things like render_to_string() return $args->{handler} = $c->app->renderer->default_handler unless exists $args->{handler}; # Call _render() for errors my $status = $args->{status} || $c->stash('status') || '200'; if ($handler eq 'openapi' and ($status eq '404' or $status eq '500')) { $args->{handler} = 'openapi'; $args->{status} = $status; $c->stash( status => $args->{status}, openapi => { errors => [{message => $c->res->default_message($args->{status}) . '.', path => '/'}], status => $args->{status}, } ); } } sub _build_route { my ($self, $app, $config) = @_; my $validator = $self->validator; my $base_path = $validator->base_url->path->to_string; my $route = $config->{route}; $route = $route->any($base_path) if $route and !$route->pattern->unparsed; $route = $app->routes->any($base_path) unless $route; $base_path = $route->to_string; $base_path =~ s!/$!!; push @{$app->defaults->{'openapi.base_paths'}}, [$base_path, $self]; $route->to({format => undef, handler => 'openapi', 'openapi.object' => $self}); $validator->base_url($base_path); if (my $spec_route_name = $config->{spec_route_name} || $validator->get('/x-mojo-name')) { $self->{route_prefix} = "$spec_route_name."; } $self->{route_prefix} //= ''; $self->route($route); } sub _helper_get_spec { my $c = shift; my $path = shift // 'for_current'; my $self = _self($c); # Get spec by valid JSON pointer return $self->validator->get($path) if ref $path or $path =~ m!^/! or !length $path; # Find spec by current request my ($stash) = grep { $_->{'openapi.path'} } reverse @{$c->match->stack}; return undef unless $stash; my $jp = [paths => $stash->{'openapi.path'}]; push @$jp, $stash->{'openapi.method'} if $path ne 'for_path'; # Internal for now return $self->validator->get($jp); } sub _helper_reply { my $c = shift; my $status = ref $_[0] ? 200 : shift; my $output = shift; my @args = @_; Mojo::Util::deprecated( '$c->reply->openapi() is DEPRECATED in favor of $c->render(openapi => ...)'); if (UNIVERSAL::isa($output, 'Mojo::Asset')) { my $h = $c->res->headers; if (!$h->content_type and $output->isa('Mojo::Asset::File')) { my $types = $c->app->types; my $type = $output->path =~ /\.(\w+)$/ ? $types->type($1) : undef; $h->content_type($type || $types->type('bin')); } return $c->reply->asset($output); } push @args, status => $status if $status; return $c->render(@args, openapi => $output); } sub _helper_valid_input { my $c = shift; return undef if $c->res->code; return $c unless my @errors = _helper_validate($c); $c->stash(status => 400) ->render(data => $c->openapi->build_response_body({errors => \@errors, status => 400})); return undef; } sub _helper_validate { my $c = shift; my $self = _self($c); my @errors = $self->validator->validate_request([@{$c->stash}{qw(openapi.method openapi.path)}], $c->openapi->build_schema_request); $c->openapi->coerce_request_parameters( delete $c->stash->{'openapi.evaluated_request_parameters'}); return @errors; } sub _log { my ($self, $c, $dir) = (shift, shift, shift); my $log_level = $self->{log_level}; $c->app->log->$log_level( sprintf 'OpenAPI %s %s %s %s', $dir, $c->req->method, $c->req->url->path, Mojo::JSON::encode_json(@_) ); } sub _op_spec_to_route { my ($self, $op_spec, $r, $config) = @_; my $op_to = $op_spec->{'x-mojo-to'} // []; my @args = ref $op_to eq 'ARRAY' ? @$op_to : ref $op_to eq 'HASH' ? %$op_to : $op_to ? ($op_to) : (); # x-mojo-to: controller#action $r->to(shift @args) if @args and $args[0] =~ m!#!; my ($constraints, @to) = ($r->pattern->constraints); $constraints->{format} //= $config->{format} if $config->{format}; while (my $arg = shift @args) { if (ref $arg eq 'ARRAY') { %$constraints = (%$constraints, @$arg) } elsif (ref $arg eq 'HASH') { push @to, %$arg } elsif (!ref $arg and @args) { push @to, $arg, shift @args } } $r->to(@to) if @to; } sub _render { my ($renderer, $c, $output, $args) = @_; my $stash = $c->stash; return unless exists $stash->{openapi}; return unless my $self = _self($c); my $status = $args->{status} || $stash->{status} || 200; my $method_path_status = [@$stash{qw(openapi.method openapi.path)}, $status]; my $op_spec = $method_path_status->[0] && $self->validator->parameters_for_response($method_path_status); my @errors; delete $args->{encoding}; $args->{status} = $status; $stash->{format} ||= 'json'; if ($op_spec) { @errors = $self->validator->validate_response($method_path_status, $c->openapi->build_schema_response); $c->openapi->coerce_response_parameters( delete $stash->{'openapi.evaluated_response_parameters'}); $args->{status} = $errors[0]->path eq '/header/Accept' ? 400 : 500 if @errors; } elsif (ref $stash->{openapi} eq 'HASH' and ref $stash->{openapi}{errors} eq 'ARRAY') { $args->{status} ||= $stash->{openapi}{status}; @errors = @{$stash->{openapi}{errors}}; } else { $args->{status} = 501; @errors = ({message => qq(No response rule for "$status".)}); } $self->_log($c, '>>>', \@errors) if @errors; $stash->{status} = $args->{status}; $$output = $c->openapi->build_response_body( @errors ? {errors => \@errors, status => $args->{status}} : $stash->{openapi}); } sub _openapi_path_to_route_path { my ($self, $http_method, $path) = @_; my %params = map { ($_->{name}, $_) } grep { $_->{in} eq 'path' } @{$self->validator->parameters_for_request([$http_method, $path])}; $path =~ s/{([^}]+)}/{ my $name = $1; my $type = $params{$name}{'x-mojo-placeholder'} || ':'; "<$type$name>"; }/ge; return $path; } sub _self { my $c = shift; my $self = $c->stash('openapi.object'); return $self if $self; my $path = $c->req->url->path->to_string; return +(map { $_->[1] } grep { $path =~ /^$_->[0]/ } @{$c->stash('openapi.base_paths')})[0]; } 1; =encoding utf8 =head1 NAME Mojolicious::Plugin::OpenAPI - OpenAPI / Swagger plugin for Mojolicious =head1 SYNOPSIS # It is recommended to use Mojolicious::Plugin::OpenAPI with a "full app". # See the links after this example for more information. use Mojolicious::Lite; # Because the route name "echo" matches the "x-mojo-name", this route # will be moved under "basePath", resulting in "POST /api/echo" post "/echo" => sub { # Validate input request or return an error document my $c = shift->openapi->valid_input or return; # Generate some data my $data = {body => $c->req->json}; # Validate the output response and render it to the user agent # using a custom "openapi" handler. $c->render(openapi => $data); }, "echo"; # Load specification and start web server plugin OpenAPI => {url => "data:///swagger.yaml"}; app->start; __DATA__ @@ swagger.yaml swagger: "2.0" info: { version: "0.8", title: "Echo Service" } schemes: ["https"] basePath: "/api" paths: /echo: post: x-mojo-name: "echo" parameters: - { in: "body", name: "body", schema: { type: "object" } } responses: 200: description: "Echo response" schema: { type: "object" } See L or L for more in depth information about how to use L with a "full app". Even with a "lite app" it can be very useful to read those guides. Looking at the documentation for L can be especially useful. (The logic is the same for OpenAPIv2 and OpenAPIv3) =head1 DESCRIPTION L is L that add routes and input/output validation to your L application based on a OpenAPI (Swagger) specification. This plugin supports both version L<2.0|/schema> and L<3.x|/schema>, though 3.x I have some missing features. Have a look at the L for references to plugins and other useful documentation. Please report in L or open pull requests to enhance the 3.0 support. =head1 HELPERS =head2 openapi.spec $hash = $c->openapi->spec($json_pointer) $hash = $c->openapi->spec("/info/title") $hash = $c->openapi->spec; Returns the OpenAPI specification. A JSON Pointer can be used to extract a given section of the specification. The default value of C<$json_pointer> will be relative to the current operation. Example: { "paths": { "/pets": { "get": { // This datastructure is returned by default } } } } =head2 openapi.validate @errors = $c->openapi->validate; Used to validate a request. C<@errors> holds a list of L objects or empty list on valid input. Note that this helper is only for customization. You probably want L in most cases. =head2 openapi.valid_input $c = $c->openapi->valid_input; Returns the L object if the input is valid or automatically render an error document if not and return false. See L for example usage. =head1 HOOKS L will emit the following hooks on the L object. =head2 openapi_routes_added Emitted after all routes have been added by this plugin. $app->hook(openapi_routes_added => sub { my ($openapi, $routes) = @_; for my $route (@$routes) { ... } }); This hook is EXPERIMENTAL and subject for change. =head1 RENDERER This plugin register a new handler called C. The special thing about this handler is that it will validate the data before sending it back to the user agent. Examples: $c->render(json => {foo => 123}); # without validation $c->render(openapi => {foo => 123}); # with validation This handler will also use L to format the output data. The code below shows the default L which generates JSON data: $app->plugin( OpenAPI => { renderer => sub { my ($c, $data) = @_; return Mojo::JSON::encode_json($data); } } ); =head1 ATTRIBUTES =head2 route $route = $openapi->route; The parent L object for all the OpenAPI endpoints. =head2 validator $jv = $openapi->validator; Holds either a L or a L object. =head1 METHODS =head2 register $openapi = $openapi->register($app, \%config); $openapi = $app->plugin(OpenAPI => \%config); Loads the OpenAPI specification, validates it and add routes to L<$app|Mojolicious>. It will also set up L and adds a L hook for auto-rendering of error documents. The return value is the object instance, which allow you to access the L after you load the plugin. C<%config> can have: =head3 coerce See L for possible values that C can take. Default: booleans,numbers,strings The default value will include "defaults" in the future, once that is stable enough. =head3 default_response Instructions for L. (Also used for OpenAPIv3) =head3 format Set this to a default list of file extensions that your API accepts. This value can be overwritten by L. This config parameter is EXPERIMENTAL and subject for change. =head3 log_level C is used when logging invalid request/response error messages. Default: "warn". =head3 op_spec_to_route C can be provided if you want to add route definitions without using "x-mojo-to". Example: $app->plugin(OpenAPI => {op_spec_to_route => sub { my ($plugin, $op_spec, $route) = @_; # Here are two ways to customize where to dispatch the request $route->to(cb => sub { shift->render(openapi => ...) }); $route->to(ucfirst "$op_spec->{operationId}#handle_request"); }}); This feature is EXPERIMENTAL and might be altered and/or removed. =head3 plugins A list of OpenAPI classes to extend the functionality. Default is: L, L and L. $app->plugin(OpenAPI => {plugins => [qw(+Cors +SpecRenderer +Security)]}); You can load your own plugins by doing: $app->plugin(OpenAPI => {plugins => [qw(+SpecRenderer My::Cool::OpenAPI::Plugin)]}); =head3 renderer See L. =head3 route C can be specified in case you want to have a protected API. Example: $app->plugin(OpenAPI => { route => $app->routes->under("/api")->to("user#auth"), url => $app->home->rel_file("cool.api"), }); =head3 skip_validating_specification Used to prevent calling L for the specification. =head3 spec_route_name Name of the route that handles the "basePath" part of the specification and serves the specification. Defaults to "x-mojo-name" in the specification at the top level. =head3 spec, url See L for the different C formats that is accepted. C is an alias for "url", which might make more sense if your specification is written in perl, instead of JSON or YAML. Here are some common uses: $app->plugin(OpenAPI => {url => $app->home->rel_file('openapi.yaml')); $app->plugin(OpenAPI => {url => 'https://example.com/swagger.json'}); $app->plugin(OpenAPI => {spec => JSON::Validator::Schema::OpenAPIv3->new(...)}); $app->plugin(OpenAPI => {spec => {swagger => "2.0", paths => {...}, ...}}); =head3 version_from_class Can be used to overridden C in the API specification, from the return value from the C method in C. Defaults to the current C<$app>. This can be disabled by setting the "version_from_class" to zero (0). =head1 AUTHORS =head2 Project Founder Jan Henning Thorsen - C =head2 Contributors =over 2 =item * Bernhard Graf =item * Doug Bell =item * Ed J =item * Henrik Andersen =item * Henrik Andersen =item * Ilya Rassadin =item * Jan Henning Thorsen =item * Jan Henning Thorsen =item * Ji-Hyeon Gim =item * Joel Berger =item * Krasimir Berov =item * Lars Thegler =item * Lee Johnson =item * Linn-Hege Kristensen =item * Manuel =item * Martin Renvoize =item * Mohammad S Anwar =item * Nick Morrott =item * Renee =item * Roy Storey =item * SebMourlhou <35918953+SebMourlhou@users.noreply.github.com> =item * SebMourlhou =item * SebMourlhou =item * Søren Lund =item * Stephan Hradek =item * Stephan Hradek =back =head1 COPYRIGHT AND LICENSE Copyright (C) Jan Henning Thorsen This program is free software, you can redistribute it and/or modify it under the terms of the Artistic License version 2.0. =head1 SEE ALSO =over 2 =item * L Guide for how to use this plugin with OpenAPI version 2.0 spec. =item * L Guide for how to use this plugin with OpenAPI version 3.0 spec. =item * L Plugin to add Cross-Origin Resource Sharing (CORS). =item * L Plugin for handling security definitions in your schema. =item * L Plugin for exposing your spec in human readable or JSON format. =item * L Official OpenAPI website. =back =cut Mojolicious-Plugin-OpenAPI-5.11/lib/Mojolicious/Plugin/OpenAPI/0000755000175100001660000000000014766301352023526 5ustar runnerdockerMojolicious-Plugin-OpenAPI-5.11/lib/Mojolicious/Plugin/OpenAPI/Guides/0000755000175100001660000000000014766301352024746 5ustar runnerdockerMojolicious-Plugin-OpenAPI-5.11/lib/Mojolicious/Plugin/OpenAPI/Guides/OpenAPIv2.pod0000644000175100001660000002046214766301334027161 0ustar runnerdocker=head1 NAME Mojolicious::Plugin::OpenAPI::Guides::OpenAPIv2 - Mojolicious <3 OpenAPI v2 (Swagger) =head1 OVERVIEW This guide will give you an introduction to how to use L with OpenAPI version v2. =head1 TUTORIAL =head2 Specification This plugin reads an L and generate routes and input/output rules from it. See L for L. { "swagger": "2.0", "info": { "version": "1.0", "title": "Some awesome API" }, "basePath": "/api", "paths": { "/pets": { "get": { "operationId": "getPets", "x-mojo-name": "get_pets", "x-mojo-to": "pet#list", "summary": "Finds pets in the system", "parameters": [ {"in": "body", "name": "body", "schema": {"type": "object"}}, {"in": "query", "name": "age", "type": "integer"} ], "responses": { "200": { "description": "Pet response", "schema": { "type": "object", "properties": { "pets": { "type": "array", "items": { "type": "object" } } } } } } } } } } The complete HTTP request for getting the "pet list" will be C The first part of the path ("/api") comes from C, the second part comes from the keys under C, and the HTTP method comes from the keys under C. The different parts of the specification can also be retrieved as JSON using the "OPTIONS" HTTP method. Example: OPTIONS /api/pets OPTIONS /api/pets?method=get Note that the use of "OPTIONS" is EXPERIMENTAL, and subject to change. Here are some more details about the different keys: =over 2 =item * swagger and info These two sections are required to make the specification valid. Check out L for a complete reference to the specification. =item * host, schemes, consumes, produces, security and securityDefinitions These keys are currently not in use. "host" will be replaced by the "Host" header in the request. The rest of the keys are currently not in use. Submit an L if you have ideas on what to use these keys for. =item * basePath The C will also be used to add a route that renders back the specification either as JSON or HTML. Examples: =over 2 =item * http://example.com/api.html Retrieve the expanded version of the API in human readable format. The formatting is currently a bit rough, but should be easier than reading the JSON spec. =item * http://example.com/api.json Retrieve the expanded version of the API, useful for JavaScript clients and other client side applications. =back =item * parameters and responses C and C will be used to define input and output validtion rules, which is used by L and when rendering the response back to the client, using C<< render(openapi => ...) >>. Have a look at L for more details about output rendering. =item * operationId and x-mojo-name See L. =item * x-mojo-placeholder C can be used inside a parameter definition to instruct Mojolicious to parse a path part in a certain way. Example: "parameters": [ { "x-mojo-placeholder": "#", "in": "path", "name": "email", "type": "string" } ] See L for more information about "standard", "relaxed" and "wildcard" placeholders. The default is to use the "standard" ("/:foo") placeholder. =item * x-mojo-to The non-standard part in the spec above is "x-mojo-to". The "x-mojo-to" key can be either a plain string, object (hash) or an array. The string and hash will be passed directly to L, while the array ref will be flatten. Examples: "x-mojo-to": "pet#list" $route->to("pet#list"); "x-mojo-to": {"controller": "pet", "action": "list", "foo": 123} $route->to({controller => "pet", action => "list", foo => 123); "x-mojo-to": ["pet#list", {"foo": 123}, ["format": ["json"]]] $route->to("pet#list", {foo => 123}); $route->pattern->constraints->{format} = ["json"]; =back =head2 Application package Myapp; use Mojo::Base "Mojolicious"; sub startup { my $app = shift; $app->plugin("OpenAPI" => {url => $app->home->rel_file("myapi.json")}); } 1; The first thing in your code that you need to do is to load this plugin and the L. See L for information about what the plugin config can be. See also L for example L application. =head2 Controller package Myapp::Controller::Pet; use Mojo::Base "Mojolicious::Controller"; sub list { # Do not continue on invalid input and render a default 400 # error document. my $c = shift->openapi->valid_input or return; # You might want to introspect the specification for the current route my $spec = $c->openapi->spec; unless ($spec->{'x-opening-hour'} == (localtime)[2]) { return $c->render(openapi => [], status => 498); } my $age = $c->param("age"); my $body = $c->req->json; # $output will be validated by the OpenAPI spec before rendered my $output = {pets => [{name => "kit-e-cat"}]}; $c->render(openapi => $output); } 1; The input will be validated using L while the output is validated through then L handler. =head2 Route names Routes will get its name from either L or from L if defined in the specification. The route name can also be used the other way around, to find already defined routes. This is especially useful for L apps. Note that if L then all the route names will have that value as prefix: spec_route_name = "my_cool_api" operationId or x-mojo-name = "Foo" Route name = "my_cool_api.Foo" You can also set "x-mojo-name" in the spec, instead of passing L to L: { "swagger": "2.0", "info": { "version": "1.0", "title": "Some awesome API" }, "x-mojo-name": "my_cool_api" } =head2 Default response schema A default response definition will be added to the API spec, unless it's already defined. This schema will at least be used for invalid input (400 - Bad Request) and invalid output (500 - Internal Server Error), but can also be used in other cases. See L and L for more details on how to configure these settings. The response schema will be added to your spec like this, unless already defined: { ... "definitions": { ... "DefaultResponse": { "type": "object", "required": ["errors"], "properties": { "errors": { "type": "array", "items": { "type": "object", "required": ["message"], "properties": {"message": {"type": "string"}, "path": {"type": "string"}} } } } } } } The "errors" key will contain one element for all the invalid data, and not just the first one. The useful part for a client is mostly the "path", while the "message" is just to add some human readable debug information for why this request/response failed. =head2 Rendering binary data Rendering assets and binary data should be accomplished by using the standard L tools: sub get_image { my $c = shift->openapi->valid_input or return; my $asset = Mojo::Asset::File->new(path => "image.jpeg"); $c->res->headers->content_type("image/jpeg"); $c->reply->asset($asset); } =head1 SEE ALSO L, L. =cut Mojolicious-Plugin-OpenAPI-5.11/lib/Mojolicious/Plugin/OpenAPI/Guides/OpenAPIv3.pod0000644000175100001660000002441114766301334027160 0ustar runnerdocker=head1 NAME Mojolicious::Plugin::OpenAPI::Guides::OpenAPIv3 - Mojolicious <3 OpenAPI v3 =head1 OVERVIEW This guide will give you an introduction on how to use L with OpenAPI version v3.x. =head1 TUTORIAL =head2 Specification This plugin reads an L and generates routes and input/output rules from it. See L for L. { "openapi": "3.0.2", "info": { "version": "1.0", "title": "Some awesome API" }, "paths": { "/pets": { "get": { "operationId": "getPets", "x-mojo-name": "get_pets", "x-mojo-to": "pet#list", "summary": "Finds pets in the system", "parameters": [ { "in": "query", "name": "age", "schema": { "type": "integer" } } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object" } } } }, "responses": { "200": { "description": "Pet response", "content": { "application/json": { "schema": { "type": "object", "properties": { "pets": { "type": "array", "items": { "type": "object" } } } } } } } } } } }, "servers": [ { "url": "/api" } ] } The complete HTTP request for getting the "pet list" will be C The first part of the path ("/api") comes from C, the second part comes from the keys under C, and the HTTP method comes from the keys under C. The different parts of the specification can also be retrieved as JSON using the "OPTIONS" HTTP method. Example: OPTIONS /api/pets OPTIONS /api/pets?method=get Note that the use of "OPTIONS" is EXPERIMENTAL, and subject to change. Here are some more details about the different keys: =over 2 =item * openapi, info and paths These three sections are required to make the specification valid. Check out L for a complete reference to the specification. =item * parameters, requestBody and responses C, C and C will be used to define input and output validation rules, which is used by L and when rendering the response back to the client, using C<< render(openapi => ...) >>. Here OpenAPIv3 input differs from the v2 spec, where C is used for input in the path or query of the request. The C is used for input passed in the body. Have a look at L for more details about output rendering. =item * operationId and x-mojo-name See L. =item * x-mojo-placeholder C can be used inside a parameter definition to instruct Mojolicious to parse a path part in a certain way. Example: "parameters": [ { "x-mojo-placeholder": "#", "in": "path", "name": "email", "type": "string" } ] See L for more information about "standard", "relaxed" and "wildcard" placeholders. The default is to use the "standard" ("/:foo") placeholder. =item * x-mojo-to The non-standard part in the spec above is "x-mojo-to". The "x-mojo-to" key can be either a plain string, object (hash) or an array. The string and hash will be passed directly to L, while the array ref will be flattened. Examples: "x-mojo-to": "pet#list" $route->to("pet#list"); "x-mojo-to": {"controller": "pet", "action": "list", "foo": 123} $route->to({controller => "pet", action => "list", foo => 123); "x-mojo-to": ["pet#list", {"foo": 123}, ["format": ["json"]]] $route->to("pet#list", {foo => 123}); $route->pattern->constraints->{format} = ["json"]; =item * security and securitySchemes The securityScheme is added under components, where one way is to have the client place an apiKey in the header of the request { ... "components": { "securitySchemes": { "apiKey": { "name": "X-Api-Key", "in": "header", "type": "apiKey" } } } } It is then referenced under the path object as security like this { ... "paths": { "/pets": { "get": { "operationId": "getPets", ... "security": [ { "apiKey": [] } ] } } } } You can then utilize security, by adding a security callback when loading the plugin $self->plugin( OpenAPI => { spec => $self->static->file("openapi.json")->path, security => { apiKey => sub { my ($c, $definition, $scopes, $cb) = @_; if (my $key = $c->tx->req->content->headers->header('X-Api-Key')) { if (got_valid_api_key()) { return $c->$cb(); } else { return $c->$cb('Api Key not valid'); } } else { return $c->$cb('Api Key header not present'); } } } } ); =back =head3 References with files Only a file reference like "$ref": "my-other-cool-component.json#/components/schemas/inputSchema" Is supported, though a valid path must be used for both the reference and in the referenced file, in order to produce a valid spec output. See L for unsupported file references =head2 Application package Myapp; use Mojo::Base "Mojolicious"; sub startup { my $app = shift; $app->plugin("OpenAPI" => {url => $app->home->rel_file("myapi.json")}); } 1; The first thing in your code that you need to do is to load this plugin and the L. See L for information about what the plugin config can be. See also L for example L application. =head2 Controller package Myapp::Controller::Pet; use Mojo::Base "Mojolicious::Controller"; sub list { # Do not continue on invalid input and render a default 400 # error document. my $c = shift->openapi->valid_input or return; # You might want to introspect the specification for the current route my $spec = $c->openapi->spec; unless ($spec->{'x-opening-hour'} == (localtime)[2]) { return $c->render(openapi => [], status => 498); } my $age = $c->param("age"); my $body = $c->req->json; # $output will be validated by the OpenAPI spec before rendered my $output = {pets => [{name => "kit-e-cat"}]}; $c->render(openapi => $output); } 1; The input will be validated using L while the output is validated through then L handler. =head2 Route names Routes will get its name from either L or from L if defined in the specification. The route name can also be used the other way around, to find already defined routes. This is especially useful for L apps. Note that if L is used then all the route names will have that value as prefix: spec_route_name = "my_cool_api" operationId or x-mojo-name = "Foo" Route name = "my_cool_api.Foo" You can also set "x-mojo-name" in the spec, instead of passing L to L: { "openapi": "3.0.2", "info": { "version": "1.0", "title": "Some awesome API" }, "x-mojo-name": "my_cool_api" } =head2 Default response schema A default response definition will be added to the API spec, unless it's already defined. This schema will at least be used for invalid input (400 - Bad Request) and invalid output (500 - Internal Server Error), but can also be used in other cases. See L and L for more details on how to configure these settings. The response schema will be added to your spec like this, unless already defined: { ... "components": { ... "schemas": { ... "DefaultResponse": { "type": "object", "required": ["errors"], "properties": { "errors": { "type": "array", "items": { "type": "object", "required": ["message"], "properties": {"message": {"type": "string"}, "path": {"type": "string"}} } } } } } } } The "errors" key will contain one element for all the invalid data, and not just the first one. The useful part for a client is mostly the "path", while the "message" is just to add some human readable debug information for why this request/response failed. =head2 Rendering binary data Rendering assets and binary data should be accomplished by using the standard L tools: sub get_image { my $c = shift->openapi->valid_input or return; my $asset = Mojo::Asset::File->new(path => "image.jpeg"); $c->res->headers->content_type("image/jpeg"); $c->reply->asset($asset); } =head1 OpenAPIv2 to OpenAPIv3 conversion Both online and offline tools are available. One example is of this is L =head1 Known issues =head2 File references Relative file references like the following "$ref": "my-cool-component.json#" "$ref": "my-cool-component.json" Will also be placed under '#/definitions/...', again producing a spec output which will not pass validation. =head1 SEE ALSO L, L. =cut Mojolicious-Plugin-OpenAPI-5.11/lib/Mojolicious/Plugin/OpenAPI/Guides/Swagger2.pod0000644000175100001660000000336614766301334027143 0ustar runnerdocker=head1 NAME Mojolicious::Plugin::OpenAPI::Guides::Swagger2 - Swagger2 back compat guide =head1 OVERVIEW This guide is useful if your application is already using L. The old plugin used to pass on C<$args> and C<$cb> to the action. This can be emulated using an L hook. The L below contains example code that you can use to make your old controllers and actions work with L. =head1 SYNOPSIS package MyApp; use Mojo::Base "Mojolicious"; sub startup { my $self = shift; # Load your specification $self->plugin("OpenAPI" => {url => $app->home->rel_file("myapi.json")}); $self->hook(around_action => sub { my ($next, $c, $action, $last) = @_; # Do not call the action with ($args, $cb) unless it is an # OpenAPI endpoint. return $next->() unless $last; return $next->() unless $c->openapi->spec; # Render error document unless the input is valid return unless $c->openapi->valid_input; my $cb = sub { my ($c, $data, $code) = @_; $c->render(openapi => $data, status => $code); }; # Call the action with ($args, $cb) # NOTE! $c->validation->output will be removed in the future return $c->$action($c->validation->output, $cb); }); } =head1 MOVING FORWARD Note that the C hook above does not prevent you from writing new actions using the standard L API. In the new actions, you can simply drop using C<$args> and C<$cb> and it will work as expected as well. =head1 SEE ALSO L L. =cut Mojolicious-Plugin-OpenAPI-5.11/lib/Mojolicious/Plugin/OpenAPI/Security.pm0000644000175100001660000001562414766301352025703 0ustar runnerdockerpackage Mojolicious::Plugin::OpenAPI::Security; use Mojo::Base -base; my %DEF_PATH = ('openapiv2' => '/securityDefinitions', 'openapiv3' => '/components/securitySchemes'); sub register { my ($self, $app, $config) = @_; my $openapi = $config->{openapi}; my $handlers = $config->{security} or return; return unless $openapi->validator->get($DEF_PATH{$openapi->validator->moniker}); return $openapi->route( $openapi->route->under('/')->to(cb => $self->_build_action($openapi, $handlers))); } sub _build_action { my ($self, $openapi, $handlers) = @_; my $global = $openapi->validator->get('/security') || []; my $definitions = $openapi->validator->get($DEF_PATH{$openapi->validator->moniker}); return sub { my $c = shift; return 1 if $c->req->method eq 'OPTIONS' and $c->match->stack->[-1]{'openapi.default_options'}; my $spec = $c->openapi->spec || {}; my @security_or = @{$spec->{security} || $global}; my ($sync_mode, $n_checks, %res) = (1, 0); my $security_completed = sub { my ($i, $status, @errors) = (0, 401); SECURITY_AND: for my $security_and (@security_or) { my @e; for my $name (sort keys %$security_and) { my $error_path = sprintf '/security/%s/%s', $i, _pointer_escape($name); push @e, ref $res{$name} ? $res{$name} : {message => $res{$name}, path => $error_path} if defined $res{$name}; } # Authenticated # Cannot call $c->continue() in case this callback was called # synchronously, since it will result in an infinite loop. unless (@e) { return if eval { $sync_mode || $c->continue || 1 }; chomp $@; $c->app->log->error($@); @errors = ({message => 'Internal Server Error.', path => '/'}); $status = 500; last SECURITY_AND; } # Not authenticated push @errors, @e; $i++; } $status = $c->stash('status') || $status if $status < 500; $c->render(openapi => {errors => \@errors}, status => $status); $n_checks = -1; # Make sure we don't render twice }; for my $security_and (@security_or) { for my $name (sort keys %$security_and) { my $security_cb = $handlers->{$name}; if (!$security_cb) { $res{$name} = {message => "No security callback for $name."} unless exists $res{$name}; } elsif (!exists $res{$name}) { $res{$name} = undef; $n_checks++; # $security_cb is obviously called synchronously, but the callback # might also be called synchronously. We need the $sync_mode guard # to make sure that we do not call continue() if that is the case. $c->$security_cb( $definitions->{$name}, $security_and->{$name}, sub { $res{$name} //= $_[1]; $security_completed->() if --$n_checks == 0; } ); } } } # If $security_completed was called already, then $n_checks will zero and # we return "1" which means we are in synchronous mode. When running async, # we need to asign undef() to $sync_mode, since it is used inside # $security_completed to call $c->continue() return $sync_mode = $n_checks ? undef : 1; }; } sub _pointer_escape { local $_ = shift; s/~/~0/g; s!/!~1!g; $_; } 1; =encoding utf8 =head1 NAME Mojolicious::Plugin::OpenAPI::Security - OpenAPI plugin for securing your API =head1 DESCRIPTION This plugin will allow you to use the security features provided by the OpenAPI specification. Note that this is currently EXPERIMENTAL! Please let me know if you have any feedback. See L for a complete discussion. =head1 TUTORIAL =head2 Specification Here is an example specification that use L and L from the OpenAPI spec: { "swagger": "2.0", "info": { "version": "0.8", "title": "Super secure" }, "schemes": [ "https" ], "basePath": "/api", "securityDefinitions": { "dummy": { "type": "apiKey", "name": "Authorization", "in": "header", "description": "dummy" } }, "paths": { "/protected": { "post": { "x-mojo-to": "super#secret_resource", "security": [{"dummy": []}], "parameters": [ { "in": "body", "name": "body", "schema": { "type": "object" } } ], "responses": { "200": {"description": "Echo response", "schema": { "type": "object" }}, "401": {"description": "Sorry mate", "schema": { "type": "array" }} } } } } } =head2 Application The specification above can be dispatched to handlers inside your L application. The do so, add the "security" key when loading the plugin, and reference the "securityDefinitions" name inside that to a callback. In this example, we have the "dummy" security handler: package Myapp; use Mojo::Base "Mojolicious"; sub startup { my $app = shift; $app->plugin(OpenAPI => { url => "data:///security.json", security => { dummy => sub { my ($c, $definition, $scopes, $cb) = @_; return $c->$cb() if $c->req->headers->authorization; return $c->$cb('Authorization header not present'); } } }); } 1; C<$c> is a L object. C<$definition> is the security definition from C. C<$scopes> is the Oauth scopes, which in this case is just an empty array ref, but it will contain the value for "security" under the given HTTP method. Call C<$cb> with C or no argument at all to indicate pass. Call C<$cb> with a defined value (usually a string) to indicate that the check has failed. When none of the sets of security restrictions are satisfied, the standard OpenAPI structure is built using the values passed to the callbacks as the messages and rendered to the client with a status of 401. Note that the callback must be called or the dispatch will hang. See also L for example L application. =head2 Controller Your controllers and actions are unchanged. The difference in behavior is that the action simply won't be called if you fail to pass the security tests. =head2 Exempted routes All of the routes created by the plugin are protected by the security definitions with the following exemptions. The base route that renders the spec/documentation is exempted. Additionally, when a route does not define its own C handler a documentation endpoint is generated which is exempt as well. =head1 METHODS =head2 register Called by L. =head1 SEE ALSO L. =cut Mojolicious-Plugin-OpenAPI-5.11/lib/Mojolicious/Plugin/OpenAPI/Cors.pm0000644000175100001660000003277514766301352025010 0ustar runnerdockerpackage Mojolicious::Plugin::OpenAPI::Cors; use Mojo::Base -base; require Mojolicious::Routes::Route; my $methods = Mojolicious::Routes::Route->can('methods') ? 'methods' : 'via'; use constant DEBUG => $ENV{MOJO_OPENAPI_DEBUG} || 0; our %SIMPLE_METHODS = map { ($_ => 1) } qw(GET HEAD POST); our %SIMPLE_CONTENT_TYPES = map { ($_ => 1) } qw(application/x-www-form-urlencoded multipart/form-data text/plain); our %SIMPLE_HEADERS = map { (lc $_ => 1) } qw(Accept Accept-Language Content-Language Content-Type DPR Downlink Save-Data Viewport-Width Width); our %PREFLIGHTED_CONTENT_TYPES = %SIMPLE_CONTENT_TYPES; our %PREFLIGHTED_METHODS = map { ($_ => 1) } qw(CONNECT DELETE OPTIONS PATCH PUT TRACE); my $X_RE = qr{^x-}; sub register { my ($self, $app, $config) = @_; my $openapi = $config->{openapi}; if ($config->{add_preflighted_routes}) { $app->plugins->once(openapi_routes_added => sub { $self->_add_preflighted_routes($app, @_) }); } my %defaults = ( openapi_cors_allowed_origins => [], openapi_cors_default_exchange_callback => \&_default_cors_exchange_callback, openapi_cors_default_max_age => 1800, ); $app->defaults($_ => $defaults{$_}) for grep { !$app->defaults($_) } keys %defaults; $app->helper('openapi.cors_exchange' => sub { $self->_exchange(@_) }); } sub _add_preflighted_routes { my ($self, $app, $openapi, $routes) = @_; my $c = $app->build_controller; my $match = Mojolicious::Routes::Match->new(root => $app->routes); for my $route (@$routes) { my $route_path = $route->to_string; next if $self->_takeover_exchange_route($route); next if $match->find($c, {method => 'options', path => $route_path}); # Make a given action also handle OPTIONS push @{$route->$methods}, 'OPTIONS'; $route->to->{'openapi.cors_preflighted'} = 1; warn "[OpenAPI] Add route options $route_path (@{[$route->name // '']})\n" if DEBUG; } } sub _default_cors_exchange_callback { my $c = shift; my $allowed = $c->stash('openapi_cors_allowed_origins') || []; my $origin = $c->req->headers->origin // ''; return scalar(grep { $origin =~ $_ } @$allowed) ? undef : '/Origin'; } sub _exchange { my ($self, $c) = (shift, shift); my $cb = shift || $c->stash('openapi_cors_default_exchange_callback'); # Not a CORS request unless (defined $c->req->headers->origin) { my $method = $c->req->method; _render_bad_request($c, 'OPTIONS is only for preflighted CORS requests.') if $method eq 'OPTIONS' and $c->match->endpoint->to->{'openapi.cors_preflighted'}; return $c; } my $type = $self->_is_simple_request($c) || $self->_is_preflighted_request($c) || 'real'; $c->stash(openapi_cors_type => $type); my $errors = $c->$cb; return _render_bad_request($c, $errors) if $errors; _set_default_headers($c); return $type eq 'preflighted' ? $c->tap('render', data => '', status => 200) : $c; } sub _is_preflighted_request { my ($self, $c) = @_; my $req_h = $c->req->headers; return undef unless $c->req->method eq 'OPTIONS'; return 'preflighted' if $req_h->header('Access-Control-Request-Headers'); return 'preflighted' if $req_h->header('Access-Control-Request-Method'); my $ct = lc($req_h->content_type || ''); return 'preflighted' if $ct and $PREFLIGHTED_CONTENT_TYPES{$ct}; return undef; } sub _is_simple_request { my ($self, $c) = @_; return undef unless $SIMPLE_METHODS{$c->req->method}; my $req_h = $c->req->headers; my @names = grep { !$SIMPLE_HEADERS{lc($_)} } @{$req_h->names}; return undef if @names; my $ct = lc $req_h->content_type || ''; return undef if $ct and $SIMPLE_CONTENT_TYPES{$ct}; return 'simple'; } sub _render_bad_request { my ($c, $errors) = @_; $errors = [{message => "Invalid $1 header.", path => $errors}] if !ref $errors and $errors =~ m!^/([\w-]+)!; $errors = [{message => $errors, path => '/'}] unless ref $errors; return $c->tap('render', openapi => {errors => $errors, status => 400}, status => 400); } sub _set_default_headers { my $c = shift; my $req_h = $c->req->headers; my $res_h = $c->res->headers; unless ($res_h->access_control_allow_origin) { $res_h->access_control_allow_origin($req_h->origin); } return unless $c->stash('openapi_cors_type') eq 'preflighted'; unless ($res_h->header('Access-Control-Allow-Headers')) { $res_h->header( 'Access-Control-Allow-Headers' => $req_h->header('Access-Control-Request-Headers') // ''); } unless ($res_h->header('Access-Control-Allow-Methods')) { my $op_spec = $c->openapi->spec('for_path'); my @methods = sort grep { !/$X_RE/ } keys %{$op_spec || {}}; $res_h->header('Access-Control-Allow-Methods' => uc join ', ', @methods); } unless ($res_h->header('Access-Control-Max-Age')) { $res_h->header('Access-Control-Max-Age' => $c->stash('openapi_cors_default_max_age')); } } sub _takeover_exchange_route { my ($self, $route) = @_; my $defaults = $route->to; return 0 if $defaults->{controller}; return 0 unless $defaults->{action} and $defaults->{action} eq 'openapi_plugin_cors_exchange'; return 0 unless grep { $_ eq 'OPTIONS' } @{$route->$methods}; $defaults->{cb} = sub { my $c = shift; $c->openapi->valid_input or return; $c->req->headers->origin or return _render_bad_request($c, '/Origin'); $c->stash(openapi_cors_type => 'preflighted'); _set_default_headers($c); $c->render(data => '', status => 200); }; return 1; } 1; =encoding utf8 =head1 NAME Mojolicious::Plugin::OpenAPI::Cors - OpenAPI plugin for Cross-Origin Resource Sharing =head1 SYNOPSIS =head2 Application Set L to 1, if you want "Preflighted" CORS requests to be sent to your already existing actions. $app->plugin(OpenAPI => {add_preflighted_routes => 1, %openapi_parameters}); See L for what C<%openapi_parameters> might contain. =head2 Simple exchange The following example will automatically set default CORS response headers after validating the request against L: package MyApp::Controller::User; sub get_user { my $c = shift->openapi->cors_exchange->openapi->valid_input or return; # Will only run this part if both the cors_exchange and valid_input was successful. $c->render(openapi => {user => {}}); } =head2 Using the specification It's possible to enable preflight and simple CORS support directly in the specification. Here is one example: "/user/{id}/posts": { "parameters": [ { "in": "header", "name": "Origin", "type": "string", "pattern": "https?://example.com" } ], "options": { "x-mojo-to": "#openapi_plugin_cors_exchange", "responses": { "200": { "description": "Cors exchange", "schema": { "type": "string" } } } }, "put": { "x-mojo-to": "user#add_post", "responses": { "200": { "description": "Add a new post.", "schema": { "type": "object" } } } } } The special part can be found in the "OPTIONS" request It has the C key set to "#openapi_plugin_cors_exchange". This will enable L to take over the route and add a custom callback to validate the input headers using regular OpenAPI rules and respond with a "200 OK" and the default headers as listed under L if the input is valid. The only extra part that needs to be done in the C action is this: sub add_post { my $c = shift->openapi->valid_input or return; # Need to respond with a "Access-Control-Allow-Origin" header if # the input "Origin" header was validated $c->res->headers->access_control_allow_origin($c->req->headers->origin) if $c->req->headers->origin; # Do the rest of your custom logic $c->respond(openapi => {}); } =head2 Custom exchange If you need full control, you must pass a callback to L: package MyApp::Controller::User; sub get_user { # Validate incoming CORS request with _validate_cors() my $c = shift->openapi->cors_exchange("_validate_cors")->openapi->valid_input or return; # Will only run this part if both the cors_exchange and valid_input was # successful. $c->render(openapi => {user => {}}); } # This method must return undef on success. Any true value will be used as an error. sub _validate_cors { my $c = shift; my $req_h = $c->req->headers; my $res_h = $c->res->headers; # The following "Origin" header check is the same for both simple and # preflighted. return "/Origin" unless $req_h->origin =~ m!^https?://whatever.example.com!; # The following checks are only valid if preflighted... # Check the Access-Control-Request-Headers header my $headers = $req_h->header('Access-Control-Request-Headers'); return "Bad stuff." if $headers and $headers =~ /X-No-Can-Do/; # Check the Access-Control-Request-Method header my $method = $req_h->header('Access-Control-Request-Methods'); return "Not cool." if $method and $method eq "DELETE"; # Set the following header for both simple and preflighted on success # or just let the auto-renderer handle it. $c->res->headers->access_control_allow_origin($req_h->origin); # Set Preflighted response headers, instead of using the default if ($c->stash("openapi_cors_type") eq "preflighted") { $c->res->headers->header("Access-Control-Allow-Headers" => "X-Whatever, X-Something"); $c->res->headers->header("Access-Control-Allow-Methods" => "POST, GET, OPTIONS"); $c->res->headers->header("Access-Control-Max-Age" => 86400); } # Return undef on success. return undef; } =head1 DESCRIPTION L is a plugin for accepting Preflighted or Simple Cross-Origin Resource Sharing requests. See L for more details. This plugin is loaded by default by L. Note that this plugin currently EXPERIMENTAL! Please comment on L if you have any feedback or create a new issue. =head1 STASH VARIABLES The following "stash variables" can be set in L, L or L. =head2 openapi_cors_allowed_origins This variable should hold an array-ref of regexes that will be matched against the "Origin" header in case the default L is used. Examples: $app->defaults(openapi_cors_allowed_origins => [qr{^https?://whatever.example.com}]); $c->stash(openapi_cors_allowed_origins => [qr{^https?://whatever.example.com}]); =head2 openapi_cors_default_exchange_callback This value holds a default callback that will be used by L, unless you pass on a C<$callback>. The default provided by this plugin will simply validate the C header against L. Here is an example to allow every "Origin" $app->defaults(openapi_cors_default_exchange_callback => sub { my $c = shift; $c->res->headers->header("Access-Control-Allow-Origin" => "*"); return undef; }); =head2 openapi_cors_default_max_age Holds the default value for the "Access-Control-Max-Age" response header set by L. Examples: $app->defaults(openapi_cors_default_max_age => 86400); $c->stash(openapi_cors_default_max_age => 86400); Default value is 1800. =head2 openapi_cors_type This stash variable is available inside the callback passed on to L. It will be either "preflighted", "real" or "simple". "real" is the type that comes after "preflighted" when the actual request is sent to the server, but with "Origin" header set. =head1 HELPERS =head2 openapi.cors_exchange $c = $c->openapi->cors_exchange($callback); $c = $c->openapi->cors_exchange("MyApp::cors_validator"); $c = $c->openapi->cors_exchange("_some_controller_method"); $c = $c->openapi->cors_exchange(sub { ... }); $c = $c->openapi->cors_exchange; Used to validate either a simple CORS request, preflighted CORS request or a real request. It will be called as soon as the "Origin" request header is seen. The C<$callback> will be called with the current L object and must return an error or C on success: my $error = $callback->($c); The C<$error> must be in one of the following formats: =over 2 =item * C Returning C means that the CORS request is valid. =item * A string starting with "/" Shortcut for generating a 400 Bad Request response with a header name. Example: return "/Access-Control-Request-Headers"; =item * Any other string Used to generate a 400 Bad Request response with a completely custom message. =item * An array-ref Used to generate a completely custom 400 Bad Request response. Example: return [{message => "Some error!", path => "/Whatever"}]; return [{message => "Some error!"}]; return [JSON::Validator::Error->new]; =back On success, the following headers will be set, unless already set by C<$callback>: =over 2 =item * Access-Control-Allow-Headers Set to the header of the incoming "Access-Control-Request-Headers" header. =item * Access-Control-Allow-Methods Set to the list of HTTP methods defined in the OpenAPI spec for this path. =item * Access-Control-Allow-Origin Set to the "Origin" header in the request. =item * Access-Control-Max-Age Set to L. =back =head1 METHODS =head2 register Called by L. =head1 SEE ALSO L. =cut Mojolicious-Plugin-OpenAPI-5.11/lib/Mojolicious/Plugin/OpenAPI/Parameters.pm0000644000175100001660000001532714766301352026177 0ustar runnerdockerpackage Mojolicious::Plugin::OpenAPI::Parameters; use Mojo::Base 'Mojolicious::Plugin'; use JSON::Validator::Util qw(is_bool schema_type); use Mojo::JSON qw(encode_json decode_json); sub register { my ($self, $app, $config) = @_; $app->helper('openapi.build_response_body' => $config->{renderer} || \&_helper_build_response_body); $app->helper('openapi.build_schema_request' => \&_helper_build_schema_request); $app->helper('openapi.build_schema_response' => \&_helper_build_schema_response); $app->helper('openapi.coerce_request_parameters' => \&_helper_coerce_request_parameters); $app->helper('openapi.coerce_response_parameters' => \&_helper_coerce_response_parameters); $app->helper('openapi.parse_request_body' => \&_helper_parse_request_body); } sub _bool { return map { !is_bool($_) ? $_ : $_ ? 'true' : 'false' } @_; } sub _helper_build_response_body { my $c = shift; return $_[0]->slurp if UNIVERSAL::isa($_[0], 'Mojo::Asset'); $c->res->headers->content_type('application/json;charset=UTF-8') unless $c->res->headers->content_type; return encode_json($_[0]); } sub _helper_build_schema_request { my $c = shift; my $req = $c->req; $c->stash->{'openapi.evaluated_request_parameters'} = \my @evaluated; return { body => sub { $evaluated[@evaluated] = $c->openapi->parse_request_body($_[1]); }, formData => sub { my $name = shift; my $value = $req->body_params->every_param($name); my $n = @$value; return $evaluated[@evaluated] = {exists => 1, value => $n > 1 ? $value : $value->[0]} if $n > 0; $value = $req->upload($name); return $evaluated[@evaluated] = {exists => !!$value, value => $value && $value->size}; }, header => sub { my $name = shift; my $value = $req->headers->every_header($name); return $evaluated[@evaluated] = {exists => !!@$value, value => $value}; }, path => sub { my $name = shift; my $stash = $c->match->stack->[-1]; return $evaluated[@evaluated] = {exists => exists $stash->{$name}, value => $stash->{$name}}; }, query => sub { return $evaluated[@evaluated] = {exists => 1, value => $req->url->query->to_hash} unless my $name = shift; my $value = $req->url->query->every_param($name); my $n = @$value; return $evaluated[@evaluated] = {exists => !!$n, value => $n > 1 ? $value : $value->[0]}; }, }; } sub _helper_build_schema_response { my $c = shift; my $res = $c->res; $c->stash->{'openapi.evaluated_response_parameters'} = \my @evaluated; return { body => sub { my $res = $c->stash('openapi'); return $evaluated[@evaluated] = {accept => $c->req->headers->accept, exists => !!$res, value => $res}; }, header => sub { my $name = shift; my $value = $res->headers->every_header($name); return $evaluated[@evaluated] = {exists => !!@$value, value => $value}; }, }; } sub _helper_coerce_request_parameters { my ($c, $evaluated) = @_; my $output = $c->validation->output; my $req = $c->req; for my $i (@$evaluated) { next unless $i->{valid}; $output->{$i->{name}} = $i->{value}; $c->stash(@$i{qw(name value)}) if $i->{in} eq 'path'; $req->headers->header($i->{name}, ref $i->{value} eq 'ARRAY' ? @{$i->{value}} : $i->{value}) if $i->{in} eq 'header'; $req->url->query->merge(@$i{qw(name value)}) if $i->{in} eq 'query'; $req->params->merge(@$i{qw(name value)}) if $i->{in} eq 'query'; $req->params->merge(@$i{qw(name value)}) if $i->{in} eq 'formData'; $req->body_params->merge(@$i{qw(name value)}) if $i->{in} eq 'formData'; } } sub _helper_coerce_response_parameters { my ($c, $evaluated) = @_; my $res = $c->res; for my $i (@$evaluated) { next unless $i->{valid}; $c->stash(openapi_negotiated_content_type => $i->{content_type}) if $i->{in} eq 'body'; $res->headers->header($i->{name}, _bool(ref $i->{value} eq 'ARRAY' ? @{$i->{value}} : $i->{value})) if $i->{in} eq 'header'; } } sub _helper_parse_request_body { my ($c, $param) = @_; my $content_type = $c->req->headers->content_type || ''; my $res = {content_type => $content_type, exists => !!$c->req->body_size}; eval { $res->{value} //= $c->req->body_params->to_hash if grep { $content_type eq $_ } qw(application/x-www-form-urlencoded multipart/form-data); # Trying to use the already parsed json() or fallback to manually decoding the request # since it will make the eval {} fail on invalid json. $res->{value} //= $c->req->json // decode_json $c->req->body; 1; } or do { $res->{value} = $c->req->body; }; return $res; } 1; =encoding utf8 =head1 NAME Mojolicious::Plugin::OpenAPI::Parameters - Methods for transforming data from/to JSON::Validator::Schema =head1 DESCRIPTION L adds helpers to your L application, required by L. These helpers can be redefined in case you have special needs. =head1 HELPERS =head2 openapi.build_response_body $bytes = $c->openapi->build_response_body(Mojo::Asset->new); $bytes = $c->openapi->build_response_body($data); Takes validated data and turns it into bytes that will be used as HTTP response body. This method is useful to override, in case you want to render some other structure than JSON. =head2 openapi.build_schema_request $hash_ref = $c->openapi->build_schema_request; Builds input data for L. =head2 openapi.build_schema_response $hash_ref = $c->openapi->build_schema_response; Builds input data for L. =head2 openapi.coerce_request_parameters $c->openapi->coerce_request_parameters(\@evaluated_parameters); Used by L to write the validated data back to L and L. =head2 openapi.coerce_response_parameters $c->openapi->coerce_response_parameters(\@evaluated_parameters); Used by L to write the validated data to L. =head2 openapi.parse_request_body $hash_ref = $c->openapi->parse_request_body; Returns a structure representing the request body. The default is to parse the input as JSON: {content_type => "application/json", exists => !!$c->req->body_size, value => $c->req->json}; This method is useful to override, in case you want to parse some other structure than JSON. =head1 METHODS =head2 register $self->register($app, \%config); This method will add the L to your L C<$app>. =head1 SEE ALSO L. =cut Mojolicious-Plugin-OpenAPI-5.11/lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm0000644000175100001660000010522614766301352026453 0ustar runnerdockerpackage Mojolicious::Plugin::OpenAPI::SpecRenderer; use Mojo::Base 'Mojolicious::Plugin'; use JSON::Validator; use Mojo::JSON; use Scalar::Util qw(blessed); 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( '/', [format => [qw(html json)]], {format => undef}, 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) = @_; my $method = $c->param('method'); my $validator = $custom || Mojolicious::Plugin::OpenAPI::_self($c)->validator; return $c->render(json => {errors => [{message => 'No spec defined.'}]}, status => 404) unless my $schema = $validator->get([paths => $path, $method ? ($method) : ()]); return $c->render( json => { '$schema' => 'http://json-schema.org/draft-04/schema#', title => $validator->get([qw(info title)]) || '', description => $validator->get([qw(info description)]) || '', %{$validator->bundle({schema => $schema})->data}, $method ? (parameters => $validator->parameters_for_request([$method, $path])) : (), } ); } sub _render_spec { my ($self, $c, $path, $custom) = @_; return $self->_render_partial_spec($c, $path, $custom) if $path; my $openapi = $custom || $self->{standalone} ? undef : Mojolicious::Plugin::OpenAPI::_self($c); my $format = $c->stash('format') || 'json'; my $validator = $custom; if (!$validator and $openapi) { $validator = $openapi->{bundled} ||= $openapi->validator->bundle; $validator->base_url($c->req->url->to_abs->path($c->url_for($openapi->validator->base_url->path))); } return $c->render(json => {errors => [{message => 'No specification to render.'}]}, status => 500) unless $validator; my $operations = $validator->routes->each(sub { my $path_item = $_; $path_item->{op_spec} = $validator->get([paths => @$path_item{qw(path method)}]); $path_item->{operation_id} //= ''; $path_item; }); return $c->render(json => $validator->data) unless $format eq 'html'; return $c->render( base_url => $validator->base_url, handler => 'ep', template => 'mojolicious/plugin/openapi/layout', operations => [sort { $a->{operation_id} cmp $b->{operation_id} } @$operations], serialize => \&_serialize, slugify => sub { join '-', map { s/\W/-/g; lc } map {"$_"} @_; }, spec => $validator->data, ); } 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 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 = JSON::Validator->new->schema($petstore->to_string)->schema->bundle; $c->openapi->render_spec($path, $custom); }; =head1 DESCRIPTION L will enable L to render the specification in both HTML and JSON format. It can also be used L if you just want to render the specification, and not add any API routes to your application. See L 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 suggestions for enhancements. See L 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, $openapi_v2_schema_object); $c = $c->openapi->render_spec("/user/{id}"); Used to render the specification as either "html" or "json". Set the L 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 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. C<%config> is the same as passed on to L. 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 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 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 to C<0>. =head1 TEMPLATING Overriding templates is EXPERIMENTAL, but not very likely to break in a bad way. L 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 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 overridden by either changing the static file "mojolicious/plugin/openapi/logo.png" or set "openapi_spec_renderer_logo" in L to a custom URL. =head1 SEE ALSO L =cut __DATA__ @@ mojolicious/plugin/openapi/header.html.ep

<%= $spec->{info}{title} || 'No title' %>

Version <%= $spec->{info}{version} %> - OpenAPI <%= $spec->{swagger} || $spec->{openapi} %>

@@ mojolicious/plugin/openapi/intro.html.ep

About

% if ($spec->{info}{description}) {
%== $c->openapi->rich_text($spec->{info}{description})
% } % my $contact = $spec->{info}{contact}; % my $license = $spec->{info}{license};

License

% if ($license->{name}) {

<%= $license->{name} %>

% } else {

No license specified.

% }

Contact information

% if ($contact->{email}) {

<%= $contact->{email} %>

% } % if ($contact->{url}) {

<%= $contact->{url} %>

% } % if (exists $spec->{openapi}) {

Servers

    % for my $server (@{$spec->{servers}}){
  • <%= $server->{url} %><%= $server->{description} ? ' - '.$server->{description} : '' %>
  • % }
% } else { % my $schemes = $spec->{schemes} || ["http"]; % my $url = Mojo::URL->new("http://$spec->{host}");

Base URL

    % for my $scheme (@$schemes) { % $url->scheme($scheme);
  • <%= $url %>
  • % }
% } % if ($spec->{info}{termsOfService}) {

Terms of service

%= $spec->{info}{termsOfService}

% } @@ mojolicious/plugin/openapi/foot.html.ep @@ mojolicious/plugin/openapi/footer.html.ep @@ mojolicious/plugin/openapi/head.html.ep <%= $spec->{info}{title} || 'No title' %> %= include 'mojolicious/plugin/openapi/style' @@ mojolicious/plugin/openapi/human.html.ep % if ($op_spec->{summary}) {

<%= $op_spec->{summary} %>

% } % if ($op_spec->{description}) {
<%== $c->openapi->rich_text($op_spec->{description}) %>
% } % if (!$op_spec->{description} and !$op_spec->{summary}) {

This resource is not documented.

% } @@ mojolicious/plugin/openapi/parameters.html.ep % my $has_parameters = @{$op_spec->{parameters} || []}; % my $body;

Parameters

% if ($has_parameters) { % } % for my $p (@{$op_spec->{parameters} || []}) { % $body = $p->{schema} if $p->{in} eq 'body'; % if ($spec->{parameters}{$p->{name}}) { % } else { % } % } % if ($has_parameters) {
Name In Type Required Description
<%= $p->{name} %><%= $p->{name} %><%= $p->{in} %> <%= $p->{type} || $p->{schema}{type} %> <%= $p->{required} ? "Yes" : "No" %> <%== $p->{description} ? $c->openapi->rich_text($p->{description}) : "" %>
% } else {

This resource has no input parameters.

% } % if ($body) {

Body

<%= $serialize->($body) %>
% } % if ($op_spec->{requestBody}) {

requestBody

<%= $serialize->($op_spec->{requestBody}{content}) %>
% } @@ mojolicious/plugin/openapi/response.html.ep % while (my ($code, $res) = $c->openapi->spec_iterator($op_spec->{responses})) {

Response <%= $code %>

%= include 'mojolicious/plugin/openapi/human', op_spec => $res
<%= $serialize->($res->{schema} || $res->{content}) %>
% } @@ mojolicious/plugin/openapi/resource.html.ep % my $id = $slugify->(op => $method, $path);

"> <%= $operation_id %>

% if ($op_spec->{deprecated}) {

This resource is deprecated!

% } %= 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}) {

Parameters

% while (my ($key, $schema) = $c->openapi->spec_iterator($spec->{parameters})) { % my $id = lc $slugify->(qw(ref parameters), $key);

<%= $key %>

<%= $serialize->($schema) %>
% } % } % if ($spec->{components}) {

Components

% while (my ($type, $comp_group) = $c->openapi->spec_iterator($spec->{components})) { % while (my ($key, $comp) = $c->openapi->spec_iterator($comp_group)) {
  • <%= $key %>
  • % } % } % } % if ($spec->{definitions}) {

    Definitions

    % while (my ($key, $schema) = $c->openapi->spec_iterator($spec->{definitions})) { % my $id = lc $slugify->(qw(ref definitions), $key);

    <%= $key %>

    <%= $serialize->($schema) %>
    % } % } @@ mojolicious/plugin/openapi/resources.html.ep

    Resources

    % for my $op (@$operations) { %= include 'mojolicious/plugin/openapi/resource', %$op; % } @@ mojolicious/plugin/openapi/toc.html.ep
      % if ($spec->{info}{description}) {
    1. About
      1. License
      2. Contact
      3. Base URL
      4. % if ($spec->{info}{termsOfService}) {
      5. Terms of service
      6. % }
    2. % }
    3. Resources
        % for my $op (@$operations) {
      1. <%= $op->{operation_id} %>
      2. % }
    4. % if ($spec->{parameters}) {
    5. Parameters
        % while (my ($key) = $c->openapi->spec_iterator($spec->{parameters})) {
      1. <%= $key %>
      2. % }
    6. % } % if ($spec->{components}) {
    7. Components
        % while (my ($type, $comp_group) = $c->openapi->spec_iterator($spec->{components})) { % while (my ($key, $comp) = $c->openapi->spec_iterator($comp_group)) {
      1. <%= $key %>
      2. % } % }
    8. % } % if ($spec->{definitions}) {
    9. Definitions
        % while (my ($key) = $c->openapi->spec_iterator($spec->{definitions})) {
      1. <%= $key %>
      2. % }
    10. % }
    @@ mojolicious/plugin/openapi/layout.html.ep %= include 'mojolicious/plugin/openapi/head'
    %= include 'mojolicious/plugin/openapi/header'
    %= include 'mojolicious/plugin/openapi/intro'
    %= include 'mojolicious/plugin/openapi/resources'
    %= include 'mojolicious/plugin/openapi/references'
    %= include 'mojolicious/plugin/openapi/footer'
    %= include "mojolicious/plugin/openapi/javascript" %= include "mojolicious/plugin/openapi/foot" @@ mojolicious/plugin/openapi/javascript.html.ep @@ mojolicious/plugin/openapi/style.html.ep @@ 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 WzGUyD3///+AqxLvAAAA+XRSTlMAE0FicjwRAkfV7/j97cgzYVkE8eEqDLmiAiTkIgoTGi0yDi43 9Ps4h7HU4+txPe7l1biNQAb7TWTN/HlD/m4xCg+n6P3osRoZ2egTI8V90W/GlV7YHxfNvBMDU9P+ 7D+uILAmp7w/3cH++MUDWsNBrPxpOxzJNd852uI0Z8+ykE7xZAmu812JVfTukvl1JvljJt2o7Jgm k4SiN2hRVYORie5ZajsvD5vV/Mw6PRcHKz1PSDRQnhHLjyTovakIWg0aBp7+muXpvpOdy9T833wM R7SNOzzzy2D+a+67Q/d3z83JseZxProqSn9N3nbBgVf3G3pes1/gzccUdOrEAAAAAWJLR0T9SwmT 6QAAAAd0SU1FB+QDCAM0E/7HrXsAAApxSURBVGje1ZprWFTHGcdnUVhgF8EFFg5FRQQ0iiIqaGiI a4jCgiKKclOkrEbdsl6IKEYR0BXUghcQ74JUAzEaVxPB0IpNtRCNF6RNjW1isEZtbLRKbdpm2f3Q M3NuM3sL0SfPMf8PsDNnzpz3NzPvO5dzAPhBkjj16dvH2UX6w+564SR1dXOXyWUe/TzFtuQ5Obzc jYzc+yvENuZ5OFy9jZx8fMW25jmk9DMK8qfENufZFfAzDMQ9UGxzeqMBAwcFDR4UPCRQosQaPiQU AwlzFdvI79fQYS8NHxEePnJUxOjIMWPHRXH5ztEYiHy82GY6VlTQhJd7kGJ+/ooJKjby1YkqdM3T GwOROYttqkNNei0upocEMZlenzwlHl5MUGMgicFi2+pASVOn9fASQGhFJEtoZ5+OdUiK2MY60NQZ M1NjbIOYZs0GaelyASQjU2xr7SprztzseTm/sA0ya5Q0NwMLWZr5YptrXwMXmM3mNxaG2wJZNAQk LNbSBNH+/RJ/mZjnpRTbWrvSLVm6bHm+Of/NYdbOHjtrBRWAODy8FMqCgAKV2NY60IrJsStXvWE2 F65+ywpkzVrgwvRH0Qu/VJwdSRu8bkhxvrmkdL3FPDJrLQhMR6uSDUAntqHfI/3GWDT10X1SlvMS CbJmIpCgeBVdrgjeFPX8D/sxtXkLM4ign8wo/RUOErkC5C6G/eFRREkqKre+0IOLWse59aJtxdt3 TIgRQNZUSV2qkX+U61Q7Xzctmi22sY44tgoTxtJlC+auHs6DRK6QBqD5o6ZIIdlVSS9XksW21oES Zgkgset2FxenxsQM31M6KH7vmCqQkM7EK51qH/Qj0+R4sc21r1e2YGsR08plJfsnLJyErmRJmfmj hvbzfZXMCvKA2Oba1yYToXXbkvhLmXmoPzbQHLHs5TEvrLtHjSZBIg4CIC2orTtU39+5H/SPX5dT wbsqucujq8Q22J7GkRyVr9J5/X3gzCEPQ/1xWBc8JZa/HntQbIPtiNprMbLSADiC7c5rNkglQn/Q 2iu2xXak30mC0M78dpjAEX0YKKcQBTaKbbEdScYQZtLhVYntPIxuLiA+kiixVC+2ybYVv5I0MwoE +GAgoQ1AOSYWV6QLe6e08e13jr777rH6BiV/qK2bffw9TscDTxiYXMXJ9widqqKAruoUXQSbldJO vf/+B/jCIf44KipEybTTQg0nE7iH0k88dVppBbKRAp4eGIixCVC+o3CtZWpWNGl8tOzwUzc7s+df Zz7EbtW6p7cUwNzf4Ed7UL9NAyfQCUC1cBx+CKbPChzBTHuGupIl+BZWtzrBzHMosv6OdvaPfo/r HAAh7vgN/cH5C3/ANRxNlsFtcqLaFIM1CFS7p9QGyMcciNFPQZh5UQApZ1vjkm0QuOM+Qj/0o2Ms SNInl3HNPA9827HC0SFggLCRh0Ig8/PkFpU2n7EJYnQLcAgiv6S3DaJv5ZpCaQ/E6F2Lg9D7QkyF SSAzHSurTgMDXyZApg0AQJnHmu9+RR3KxDhts4EHuRqGxJRplbIgYYKu8SBGb1fbIIG8p3YQIFqm BmZU16QJINeXEyD5nYBKEcJvGF1LUDgB8scsoGRaK7HNSalTuDS1omknLIXiQP70Ka0/j29JhAmZ Jwuy4VNengoexKj2tQlSBLv8Bsyb7oKDfIYqcO2LFrPGOh0PQs3EObLLbtLm8ONf69cIqGF/IUD2 cF6U6MQ+QNEfHab6zOdA/specEWEfVmQk0S05EGMGY02QPRucNylfE7/9QjBQb5gEwVuyMWUPAiY g3GUfdI58hb9kBbmmDe0ORdQC78kOHpSgRR1iIdwjG3oQsO9wxJEifaWl3SOQWRdlDWIqww1TStb rzUI6ICp22cEkOv5/LAq3B/0Vk8cdOaqi+7e3n8LoSOOhYf0rB8MTqLnt+BmnYU5d/QWIIavYKpN YR8Edf3V8VJLEGkbTGmU5XCUf26wAUJtgKkrmQIIWMBy3L3XeYHer8fdl8ByabkJMDIqdg/vsfB1 aQryM+KFaBMcRLJACxAJChstbI+ctgHydxnygkBLkNxqmCqSMv+PSG30SAtMpWM9AuZsR6Oq+OtU eOwQXnogYnMUM79R179ekfwgjvCRfwAFaq08FW7WfBSyywkQqTJFzpjDgFRf4dWWxYJ8w4SNVr0F iBcKaHTg9Wc8wQrE4IQiiUaFgUwtpJ18RmfqNOgMcaV7t5h2zinpTMq63lly+W7nrrH3H+IjK4iN 8K3ES8QTaphXz4J80fXo0aOudxaj5g4NtJ5H/nmeA8m9htykgwQxoNeVdZyvtM8XQK510HU/qtcg DrqvMJCsBdnzHgftQT4d/mRrt8m0ZAbv/jNWral4MELok9cooEJPOUR8NJCJzDpkY0K82gEcgQDP GhQ5nAgQXzhSveG7JBfYQtoiAQSX9pgeYCDgXyU5I5lWj3uSTHOszJknzCtPk7s33eL7JHwwACoN cmGiRzKvMP5vDZJxwjGIoV6LBnsaBkJdgr/dEuDvOvjTJ9gmiE8IIEDAbvYFT1xpMr2Fqjz4OF+I yHdXR5p28aPrAr0WM6Bxm0Hs3XPRPJxiBSJrhdYwIMf8eNXzPkJvtTWIRNMsgBQgF/dHB7QNcAmr rbUBcvVoCGUBMogBiUuF/WFatKoQnyJnjovo3vSAOdt+uBu2F9NIxFGdM5witSEsSGgNUvtt/1oU EuxHrW/gOEJmy9oFkCboGHKnTKhctLr1N3Ag0R5M5WpNX9RjBAgYCk+u425tRVvaKU8tFi30HrLi PvST9beYyQo2odYLd5I6mOXTyIJsRSZknlGxRRyDSA/L+FZmQFDnaBmDa9BFnwAO5CxTd2Yje6BO gmRN+LJnxJNN6ICrctllAsQ8d9loU3fFA3o+GXYOlVYdRUO4gDeK+bhD26ZjQf4NSDkGAVS9nAAp IPYRSPIjHMi3FnWTIOD8sBGlB7rR1mrK/nkkyPY3/0P31NhbDy9wpWvRqtLvBJukfBFZqBN4NhCQ loeDUC1Ga90x9A4ETBrK9Iepe0lJNglivruQ3rVXVuweyBtxG94c1m886l7DEbWcG8fPBgI82zGQ tOk2QOB7/V6BAP1eBmTR/e1mSxVv3mLaUoGFKVdmOgpb3NGnT91RFHWMbnCd4Qjkv//j9FlrIwkC vEIFEBQ4jEe/5XQWXaPDfe9AgGEzOnKseGzFYS7LqYg4gL8Alday+3ott30yquGe1iEIJn7PzoGo 2rQ8SAv8KQtRcSq4A69U5/YWBEiHbKw0Ray6bA2SPXfbRPLrP12TWotbFpaHOHoL8rElCEi4zYHE o93UYpXwLOQzYV69BgEgeMnKfZ1lNjh2nAOWCriUKMSU6g4Jk/vMIKybXAQU2tvQuzFBVSgCa3QI 5KvegNAoksfz8gmK/LLLO5JsfWVGBXbd8fG4cSOx+mJRApepbP6OlpNF0QLNd6Q09IzjD3808GUU XTDdAvQt8L8f8UUCyspQ9YX/6izqPt8Gc218dDVg/73lhRxL9t3ipzkDgT0ZZjeEhDQkvLAvGqis pJyb9+7du9mZRP2Ev/j7ier/2PE6aEE6GrEAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDMtMDhU MDM6NTI6MTYrMDA6MDC7y1oBAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTAzLTA4VDAzOjUyOjE1 KzAwOjAw+374IAAAAABJRU5ErkJggg== Mojolicious-Plugin-OpenAPI-5.11/MANIFEST0000644000175100001660000000410014766301353017120 0ustar runnerdockerChanges lib/Mojolicious/Plugin/OpenAPI.pm lib/Mojolicious/Plugin/OpenAPI/Cors.pm lib/Mojolicious/Plugin/OpenAPI/Guides/OpenAPIv2.pod lib/Mojolicious/Plugin/OpenAPI/Guides/OpenAPIv3.pod lib/Mojolicious/Plugin/OpenAPI/Guides/Swagger2.pod lib/Mojolicious/Plugin/OpenAPI/Parameters.pm lib/Mojolicious/Plugin/OpenAPI/Security.pm lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm Makefile.PL MANIFEST This list of files t/00-project.t t/basic-404-501.t t/basic-autorender.t t/basic-bundle.t t/basic-coerce.t t/basic-correct-order-of-paths.t t/basic-custom-formats.t t/basic-custom-renderer.t t/basic-custom-validation.t t/basic-empty-response.t t/basic-invalid-json-input.t t/basic-legacy-swagger2.t t/basic-mojo-placeholder.t t/basic-mojo-route-names.t t/basic-path-parameters.t t/basic-register-plugin.t t/basic-under-route-authenticate.t t/data/image.jpeg t/jv-recursion.t t/plugin-cors.t t/plugin-security-extended-status.t t/plugin-security-rules-not-defined.t t/plugin-security-v2.t t/plugin-security-v3.t t/plugin-spec-renderer-doc.t t/plugin-spec-renderer-options.t t/plugin-spec-renderer-standalone.t t/plugin-spec-renderer-v3.t t/spec/bundlecheck.json t/spec/swagger/parameters/body.yaml t/spec/swagger/paths/ref.yaml t/spec/swagger/responses/ok.yaml t/spec/swagger/swagger.yaml t/spec/v2-petstore.json t/spec/v3-invalid_file_refs.yaml t/spec/v3-invalid_file_refs_no_path.yaml t/spec/v3-invalid_include.yaml t/spec/v3-petstore.json t/spec/v3-valid_file_refs.yaml t/spec/v3-valid_include.yaml t/v2-basic.t t/v2-body.t t/v2-collectionformat.t t/v2-defaults.t t/v2-discriminator.t t/v2-file.t t/v2-formats.t t/v2-headers.t t/v2-id-prop.t t/v2-readonly.t t/v2-swagger.t t/v2-tutorial.t t/v2-validate-schema.t t/v3-basic.t t/v3-body.t t/v3-bundle.t t/v3-defaults.t t/v3-file.t t/v3-invalid_file_refs.t t/v3-invalid_file_refs_no_path.t t/v3-nullable.t t/v3-style-array.t t/v3-style-object.t t/v3-tutorial.t t/v3-valid_file_refs.t t/v3-writeonly.t META.yml Module YAML meta-data (added by MakeMaker) META.json Module JSON meta-data (added by MakeMaker) Mojolicious-Plugin-OpenAPI-5.11/Makefile.PL0000644000175100001660000000510614766301334017747 0ustar runnerdockeruse 5.016; use strict; use warnings; use utf8; use ExtUtils::MakeMaker; my $GITHUB_URL = 'https://github.com/jhthorsen/mojolicious-plugin-openapi'; my %WriteMakefileArgs = ( NAME => 'Mojolicious::Plugin::OpenAPI', AUTHOR => 'Jan Henning Thorsen ', LICENSE => 'artistic_2', ABSTRACT_FROM => 'lib/Mojolicious/Plugin/OpenAPI.pm', VERSION_FROM => 'lib/Mojolicious/Plugin/OpenAPI.pm', TEST_REQUIRES => {'Test::More' => '0.88'}, PREREQ_PM => {'JSON::Validator' => '5.13', 'Mojolicious' => '9.00'}, META_MERGE => { 'dynamic_config' => 0, 'meta-spec' => {version => 2}, 'no_index' => {directory => [qw(examples t)]}, 'prereqs' => {runtime => {recommends => {'Text::Markdown' => 'v1.0.31'}, requires => {perl => '5.016'}}}, 'resources' => { bugtracker => {web => "$GITHUB_URL/issues"}, homepage => $GITHUB_URL, license => ['http://www.opensource.org/licenses/artistic-license-2.0'], repository => {type => 'git', url => "$GITHUB_URL.git", web => $GITHUB_URL}, x_IRC => { url => 'irc://irc.libera.chat/#perl-openapi', web => 'https://web.libera.chat/#perl-openapi' }, }, 'x_contributors' => [ 'Bernhard Graf ', 'Doug Bell ', 'Ed J ', 'Henrik Andersen ', 'Ilya Rassadin ', 'Jan Henning Thorsen ', 'Ji-Hyeon Gim ', 'Joel Berger ', 'Krasimir Berov ', 'Lars Thegler ', 'Lee Johnson ', 'Linn-Hege Kristensen ', 'Manuel ', 'Martin Renvoize ', 'Mohammad S Anwar ', 'Nick Morrott ', 'Renee ', 'Roy Storey ', 'SebMourlhou <35918953+SebMourlhou@users.noreply.github.com>', 'Søren Lund ', 'Stephan Hradek ', 'Stephan Hradek ', ], }, test => {TESTS => (-e 'META.yml' ? 't/*.t' : 't/*.t xt/*.t')}, ); unless (eval { ExtUtils::MakeMaker->VERSION('6.63_03') }) { my $test_requires = delete $WriteMakefileArgs{TEST_REQUIRES}; @{$WriteMakefileArgs{PREREQ_PM}}{keys %$test_requires} = values %$test_requires; } WriteMakefile(%WriteMakefileArgs); Mojolicious-Plugin-OpenAPI-5.11/Changes0000644000175100001660000004031214766301334017266 0ustar runnerdockerRevision history for perl distribution Mojolicious-Plugin-OpenAPI 5.11 2025-03-18T23:24:00 - Fix bad commit from Github 5.10 2025-03-18T23:17:00 - Fix for multiple sub paths in haproxy #250 - Fix missing handler defaults to authorize #254 #255 - Bumped JSON::Validator to 5.15 #252 5.09 2023-02-20T16:34:43 - Will only run t/00-project.t when developing #241 5.08 2022-12-09T09:58:39 - Fix not coercing body parameter for OpenAPIv2 in JSON::Validator v5.13 - Specified Perl version - Updated basic repository files - Updated contributors list 5.07 2022-08-18T07:41:35+0900 - Bumped J::V version to support DefaultResponse in $ref path object #236 5.06 2022-08-17T20:15:46+0900 - Fix incompatability with OpenAPIv2 spec regarding default "collectionFormat" 5.05 2022-03-25T10:00:49+0900 - Fix collectionFormat in header 5.04 2022-03-23T07:58:50+0900 - Depends on JSON::Validator 5.07 5.03 2022-03-23T07:44:41+0900 - Updated documentation to make it more obvious that you can pass in a schema - Updated the SYNOPSIS making it more obvious to read the guides 5.02 2021-11-21T09:22:33+0900 - Fix reading request body as string if not form data or JSON #227 5.01 2021-11-20T13:39:28+0900 - Avoid uninitialized warnings #224 - Bump JSON::Validator to 5.03 5.00 2021-10-02T10:16:28+0900 - Fix "version_from_class" uses the VERSION from $app by default - Compatible with JSON::Validator 5.00 - Removed support for "allow_invalid_ref" - Changed render_spec() to require a JSON::Validator::Schema::OpenAPIv2 object 4.06 2021-09-14T14:08:26+0200 - Add support for adding $route->to(...) programmatically - Fix link to Convos example spec #222 Contributor: Roy Storey - Cleaned up CPAN distribution files 4.05 2021-07-10T15:56:19+0900 - Allow string in OpenAPIv2 request body #219 4.04 2021-06-17T11:16:04+0900 - Depending on JSON::Validator 4.18 4.03 2021-04-28T11:32:17+0900 - Depending on JSON::Validator 4.17 - Add support for "skip_validating_specification" - Reverted back to supporting "default_response" - Moved "DefaultResponse" for v2 from "definitions" to "responses" - Improved documentation: "schema" is no longer needed 4.02 2021-03-24T11:37:27+0900 - Add support for passing in constraints (such as format) using x-mojo-to 4.01 2021-03-24T09:14:22+0900 - Using routes() from JSON::Validator::Schema::OpenAPIv2 and ::OpenAPIv3 - Fix generating correct OpenAPIv2 spec #199 - Fix not adding basePath to OpenAPIv3 spec #200 - Fix compatibility with Mojolicious 9.11 #204 4.00 2021-02-17T09:23:35+0900 - Using JSON::Validator::Schema::OpenAPIv2 and OpenAPIv2 schema API #160 - Compatible with Mojolicious 9.0 - Depends on JSON::Validator 4.13 3.41 2021-01-24T16:44:53+0900 - Add announcement about the new JSON::Validator based API - Add missing documentation about Text::Markdown - Add permalinks to headers in SpecRenderer - SpecRenderer adds openapi.rich_text helper - Compatible with Mojolicious 8.67 - Bump JSON::Validator to 4.11 - Removed $c->openapi->cors_simple() helper 3.40 2020-10-10T16:23:05+0900 - Compatible with JSON::Validator 4.06 3.39 2020-10-08T15:52:42+0900 - Add support optional requestBody in OpenAPIv3 #170 #194 Contributor: Ji-Hyeon Gim 3.38 2020-10-07T12:39:18+0900 - Fix failing CPAN testers tests - Fix uninitialized warnings - Fix not coercing body with array into object 3.37 2020-10-06T13:47:47+0900 - Can also render doc for spec with $ref inside paths - Can specify SpecRenderer logo - Can specify SpecRenderer theme color - Will block less when rendering big JSON documents in browser - Bump JSON::Validator to 4.05 3.36 2020-09-22T09:42:10+0900 - Fix writeOnly handling OpenAPI v3 #191 3.35 2020-08-11T11:32:59+0900 - Add support for v3 object parameters #184 Contributor: SebMourlhou - Add support for passing in custom spec to $c->openapi->render_spec #189 - Fix handling 404 and 501 in v3 #179 - Fix issue when "nullable" is stored inside JSON::Validator::Ref #183 - Fix $c->openapi->validate helper #187 3.34 2020-08-05T16:36:41+0900 - Can now set custom status code from a security callback #186 Contributor: Stephan Hradek 3.33 2020-06-08T15:28:12+0900 - Forgot to bump JSON::Validator to 4.00 3.32 2020-06-08T10:42:08+0900 - Compatible with JSON::Validator 4.00 3.31 2020-03-31T20:42:25+0900 - Fix SpecRenderer should not add ".json" to the "servers" URL #174 3.30 2020-03-24T10:24:44+0900 - Fix support for file uploads in OpenAPI v3 #171 3.29 2020-03-23T15:09:35+0900 - Add basic support for file uploads in OpenAPI v3 #171 3.28 2020-03-10T19:20:23+0900 - Fix setting "schemes" in OpenAPIv2 when rendering the spec - Fix links to parameters in SpecRenderer - Hiding "About" header on desktop 3.27 2020-03-09T09:33:44+0900 - Fix "up" button in SpecRenderer 3.26 2020-03-09T09:24:33+0900 - Add "up" button to SpecRenderer - Fix some rendering issues for jsonhtmlify - Merged "renderjson" and "scrollspy" into "javascript.html.ep" - Split "References" into "Parameters", "Definitions" and "Components" 3.25 2020-03-08T18:38:25+0900 - Switched to jsonhtmlify in SpecRenderer #167 - SpecRenderer styling is using colors from the OpenAPI logo - Changed to rendering operationId, instead of method/path in headings - Documented demo page and template structure for SpecRenderer - Moved "Base URL", "Contact" and "License" in SpecRenderer 3.24 2020-03-06T13:30:16+0900 - SpecRenderer can be used standalone - New default CSS styling for SpecRenderer - Add more templates for SpecRenderer * mojolicious/plugin/openapi/foot.html.ep * mojolicious/plugin/openapi/head.html.ep * mojolicious/plugin/openapi/renderjson.html.ep * mojolicious/plugin/openapi/scrollspy.html.ep * mojolicious/plugin/openapi/style.html.ep 2.23 2020-03-03T15:57:42+0900 - Started slowly to deprecate $c->validation->output #162 - Bump JSON::Validator version #163 2.22 2020-03-02T17:09:49+0900 - Improved links to v3.x documentation 2.21 2020-01-24T12:34:04+0900 - Will not detect invalid route names on startup - Add support for v3 array parameters #149 #154 Contributor: Sebastien Mourlhou 2.20 2019-12-12T21:17:07+0100 - Depends on YAML::XS because it's a nicer way to write the spec and I have made too many failed releases that depend on YAML::XS #153 2.19 2019-12-04T17:19:08+0100 - Add support for parameter defaults in OpenAPI v3 #115 - Override generate_definitions_path() in order to render proper OpenAPIv3 spec #152 - Update of OpenAPI3 guide #152 2.18 2019-10-28T14:18:33+0900 - Fix /servers/url for OpenAPI v3 in SpecRenderer #148 - Fix OpenAPI v3 parameter type #137 #147 Contributor: SebMourlhou 2.17 2019-10-17T08:12:29+0900 - Add tuturial for OpenAPI v3 #142 Contributor: Henrik Andersen - The internal doc renderer now supports OpenAPI v3 #144 Contributor: Henrik Andersen - Fixed failing tests #143 Contributor: Henrik Andersen - Fixed rendering OpenAPI v3 spec #141 Contributor: Henrik Andersen - Fixed failing integration with OpenAPI::Client #135 Contributor: Roy Storey 2.16 2019-08-02T09:07:24+0200 - Fix t/v3-body.t when YAML::XS is not available 2.15 2019-08-01T20:18:05+0200 - Add support for v3 schema from https://spec.openapis.org/oas/3.0/schema/2019-04-02 - Add support for handling of securitySchemes in OpenAPI v3 #129 Contributor: Ilya Rassadin - Fix default responses for OpenAPI v3 #129 Contributor: Ilya Rassadin - Compatible with new Mojo::Exception # 133 Contributor: Roy Storey 2.14 2019-05-05T14:11:06+0700 - Fix "coerce(1) will be deprecated" #130 - Changed OPTIONS response to be a draft-04 response - Need to bundle all responses from SpecRenderer to make OPTIONS render in a more human friendly way. - Require Mojolicious 8.00 #122 2.13 2019-03-13T17:12:52+0800 - Fix issue in OpenAPI::Security when used from OpenAPI::Client, or another UserAgent with an IOLoop that is not the singleton. #121 - Fix issue in SYNOPSIS that gave confusing output for /api Contributor: Bernhard Graf 2.12 2019-02-14T20:12:16+0100 - Fix HEAD requests #105 - Fix using /servers/0/url as basePath for OpenAPI v3 #110 Note: This could be breaking change - Fix getting basePath when using under #107 - Add support for "nullable" in OpenAPI 3.0 #106 - Improved handling of Accept header in OpenAPI v3 #104 Can now handle wildcards, such as application/* and */*, even though not defined in the specification. - Bump JSON::Validator to 3.06 2.11 2019-01-26T11:37:15+0900 - Fix allowing regular requests with "openapi_cors_allowed_origins" #103 2.10 2019-01-25T12:49:55+0900 - Add "plugins" as a documented feature for register() - Add Mojolicious::Plugin::OpenAPI::SpecRenderer - Add the possibility to turn off automatic rendering of specification using OPTIONS and from /:basePath route - Add EXPERIMENTAL "openapi_routes_added" hook - Add support for Preflight CORS requests #99 - Fix Simple CORS requests with "GET" and no Content-Type #99 - Fix writing a list of headers back after validated - Marked $c->openapi->simple_cors as DEPRECATED 2.09 2019-01-21T09:51:56+0900 - Using formats from JSON::Validator 3.04 2.08 2019-01-07T10:00:52+0900 - Fix Data::Validate::IP is an optional module for the test suite #100 - Bumping JSON::Validator to 3.01 2.07 2018-12-15T11:50:30+0900 - Merged JSON::Validator::OpenAPI into JSON::Validator::OpenAPI::Mojolicious - Compatible with "formats" in JSON::Validator 3.x 2.06 2018-12-07T14:14:24+0900 - Made YAML::XS and v3 optional 2.05 2018-12-07T14:02:49+0900 - Moved JSON::Validator::OpenAPI::Mojolicious from JSON-Validator 2.04 2018-11-15T16:13:55+0900 - Use data:///file.json in SYNOPSIS to make it work with morbo 2.03 2018-11-14T15:42:27+0900 - Improved human readable documentation rendering 2.02 2018-11-14T13:13:13+0900 - Mention EXPERIMENTAL support for OpenAPI v3 #75 2.01 2018-10-26T11:58:10+0900 - Fix default error template lookup by mode #93 Contributor: Doug Bell - Bumped JSON::Validator version to 2.14 2.00 2018-09-30T21:53:28+0900 - Add support for "default_response_codes" #66 #80 - Add support for "default_response_name" #66 #80 - Add support for plack and other servers that does not start the IOLoop #82 - Add detection for invalid x-mojo-name on startup #87 - Changed "message" in JSON response for 404, 500 and 501 - Changed "path" is not required in default error response - Removed default "default_response" #80 - Removed "Using default_handler to render..." warning since it was confusing - Bump Mojolicious version to 8.0 1.30 2018-06-06T00:20:46+0800 - Fix exception handling in an action, with the security plugin enabled 1.29 2018-06-03T20:32:21+0800 - Fix "No security callback for $name." error object - Fix "status" icompatibility with Mojolicious 7.82 #78 1.28 2018-04-21T11:03:02+0200 - Add support for Simple Cross-Origin Resource Sharing requests (CORS) #14 - Bumped JSON::Validator version - Changed placeholders from () to <> to support Mojolicious 7.75 #73 1.27 2018-04-09T09:05:10-0700 - Add EXPERIMENTAL route name for OPTIONS routes #69 - Add Text::Markdown as an optional module for rendering documentation snippets #63 Contributor: Lars Thegler 1.26 2018-03-08T21:15:52+0100 - Fix skipping yaml.t, unless correct version of YAML::XS is available #67 Contributor: Søren Lund 1.25 2018-01-29T10:00:59+0100 - Removed YAML::Syck test #60 - Change register() to return the plugin instance 1.24 2018-01-19T10:37:28+0100 - Require JSON::Validator 2.00 which fixes "enum" bug 1.23 2017-12-25T10:50:28+0100 - Fix setting default values #53 #55 - Can specify schema when loading plugin 1.22 2017-11-19T20:25:16+0100 - Compatible with JSON::Validator 1.06 - Deprecated "reply.openapi" helper - Moved security handling to separate module - Started on plugin support #14 1.21 2017-07-24T21:46:37+0200 - "path" is not required in default error document 1.20 2017-07-24T21:41:01+0200 - Add "default_response" parameter to register() 1.19 2017-07-10T22:44:19+0200 - Add support for "security" and "securityDefinitions" Contributor: Joel Berger 1.18 2017-07-04T09:23:48+0200 - Fix rendering of documentation does not die when "parameters" are under a path - Fix generating routes with "parameters" under a path #42 - Fix other documentation renderers, when "parameters" under a pth #42 1.17 2017-06-12T20:58:57+0200 - Add support for fetching API spec in route chain - Add "exception" stash variable on internal server error #38 Contributor: Manuel Mausz 1.16 2017-05-18T11:23:52+0200 - Can override status code in "renderer" function 1.15 2017-05-15T09:15:14+0200 - Fix "renderer" will also be called for internal errors #34 #35 - Removed openapi.not_implemented helper 1.14 2017-05-13T11:55:37+0200 - Fix automatically coercing values #33 Contributor: Nick Logan - Add openapi.render_spec helper - Add example for how to use a M::P::Swagger2 powered app with M::P::OpenAPI - Bump JSON::Validator version 1.13 2017-03-03T00:35:26+0100 - Forgot to bump JSON::Validator version in cpanfile #32 1.12 2017-03-02T23:10:18+0100 - Compatible with JSON::Validator 0.95 1.11 2017-03-01T19:42:58+0100 - Fix adding routes with wildcards after routes without wildcards - Add fallback to default renderer, unless "openapi" is set in stash 1.10 2017-02-21T15:35:45+0100 - Fix resolve of specification twice #19 - Require JSON::Validator 0.94 #30 1.09 2017-01-30T13:11:52+0000 - Prevent stomping of status in before_render hook 1.08 2017-01-25T17:27:12+0100 - Add EXPERIMENTAL openapi.not_implemented helper 1.07 2016-12-11T11:39:46+0100 - Compatible with JSON::Validator 0.90 1.06 2016-11-18T15:57:26+0100 - Will rewrite basePath in generated spec, relative to base URL - Documented x-mojo-placeholder #16 1.05 2016-10-26T13:23:38+0200 - Add support for path parameters #11 - Fix typos in tutorial regarding example snippets #13 - Fix default OPTIONS path, when it has placeholders 1.04 2016-10-06T21:39:06+0200 - Fix responding with an empty string #9 - Fix responding with null 1.03 2016-09-27T23:58:41+0200 - Bumped required JSON::Validator version to 0.85 #8 1.02 2016-09-27T09:52:02+0200 - Fix bug for collectionFormat handling in JSON::Validator - Add support for "version_from_class" - Add TOC to .html rendering of API 1.01 2016-09-21T16:07:45+0200 - Fix documentation regarding the "reply.openapi" helper #7 1.00 2016-09-04T15:08:56+0200 - Removed EXPERIMENTAL 0.14 2016-08-20T14:04:58+0200 - Fix rendering UTF-8 characters 0.13 2016-08-16T19:54:48+0200 - Removed $c->openapi->invalid_input() - Add support for rendering specification on OPTIONS #1 0.12 2016-08-10T21:16:54+0200 - Add support for $c->render(openapi => $data); - Started DEPRECATING $c->reply->openapi() 0.11 2016-08-09T13:35:16+0200 - Add support for retrieving the complete API spec - Improved tutorial 0.10 2016-08-07T22:16:38+0200 - Add $c->openapi->validate() - Deprecated $c->openapi->invalid_input() - Fix validating YAML specifications #3 #4 Contributor: Ilya Rassadin 0.09 2016-08-04T09:30:23+0200 - Add basic support for rendering spec as HTML - Add check for $ref in the right place in the input specification Contributor: Lari Taskula 0.08 2016-07-29T14:33:14+0200 - Add check for unique operationId and route names - All route names will have "spec_route_name." as prefix 0.07 2016-07-26T21:53:56+0200 - Add support for serving binary data 0.06 2016-07-26T18:56:50+0200 - Add support for naming baseUrl (specification) route - Add openapi.valid_input helper - Fix loading the plugin twice 0.05 2016-07-26T15:04:25+0200 - Fix "false" must be false and not true - Make sure 404 is returned as default format and not html 0.04 2016-07-25T15:03:31+0200 - Fix setting default values in JSON::Validator::OpenAPI 0.76 - Fix registering correct HTTP method for action in a class 0.03 2016-07-25T11:25:43+0200 - Add openapi.invalid_input helper - Add Mojolicious::Plugin::OpenAPI::Guides::Tutorial - Remove openapi.validate helper - Remove openapi.input helper - Will store validated data into $c->validation->output 0.02 2016-06-11T07:32:51-0700 - Improved documentation - Add support for MOJO_OPENAPI_LOG_LEVEL=error 0.01 2016-06-10T19:34:35-0700 - Add logging of request/response errors - Add rendering of API spec from base URL - Exceptions returns structured JSON data instead of HTML - Making an improved version of Mojolicious::Plugin::Swagger2 - Started project Mojolicious-Plugin-OpenAPI-5.11/META.yml0000644000175100001660000000406014766301352017244 0ustar runnerdocker--- abstract: 'OpenAPI / Swagger plugin for Mojolicious' author: - 'Jan Henning Thorsen ' build_requires: ExtUtils::MakeMaker: '0' Test::More: '0.88' configure_requires: ExtUtils::MakeMaker: '0' dynamic_config: 0 generated_by: 'ExtUtils::MakeMaker version 7.70, CPAN::Meta::Converter version 2.150010' license: artistic_2 meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: '1.4' name: Mojolicious-Plugin-OpenAPI no_index: directory: - t - inc - examples - t recommends: Text::Markdown: v1.0.31 requires: JSON::Validator: '5.13' Mojolicious: '9.00' perl: '5.016' resources: IRC: url: irc://irc.libera.chat/#perl-openapi web: https://web.libera.chat/#perl-openapi bugtracker: https://github.com/jhthorsen/mojolicious-plugin-openapi/issues homepage: https://github.com/jhthorsen/mojolicious-plugin-openapi license: http://www.opensource.org/licenses/artistic-license-2.0 repository: https://github.com/jhthorsen/mojolicious-plugin-openapi.git version: '5.11' x_contributors: - 'Bernhard Graf ' - 'Doug Bell ' - 'Ed J ' - 'Henrik Andersen ' - 'Ilya Rassadin ' - 'Jan Henning Thorsen ' - 'Ji-Hyeon Gim ' - 'Joel Berger ' - 'Krasimir Berov ' - 'Lars Thegler ' - 'Lee Johnson ' - 'Linn-Hege Kristensen ' - 'Manuel ' - 'Martin Renvoize ' - 'Mohammad S Anwar ' - 'Nick Morrott ' - 'Renee ' - 'Roy Storey ' - 'SebMourlhou <35918953+SebMourlhou@users.noreply.github.com>' - 'Søren Lund ' - 'Stephan Hradek ' - 'Stephan Hradek ' x_serialization_backend: 'CPAN::Meta::YAML version 0.018' Mojolicious-Plugin-OpenAPI-5.11/t/0000755000175100001660000000000014766301352016236 5ustar runnerdockerMojolicious-Plugin-OpenAPI-5.11/t/v3-invalid_file_refs.t0000644000175100001660000000125414766301334022417 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; get '/test' => sub { my $c = shift->openapi->valid_input or return; $c->render(status => 200, openapi => $c->param('pcversion')); }, 'File'; plugin OpenAPI => {url => app->home->rel_file('spec/v3-invalid_file_refs.yaml')}; my $t = Test::Mojo->new; $t->get_ok('/api')->status_is(200)->json_hasnt('/PCVersion/name')->json_has('/components/schemas') ->content_like(qr/v3-invalid_include_yaml-PCVersion/); my $validator = JSON::Validator::Schema::OpenAPIv3->new($t->get_ok('/api')->tx->res->body); like $validator->errors->[0], qr/Properties not allowed/, 'invalid bundled spec'; done_testing; Mojolicious-Plugin-OpenAPI-5.11/t/basic-under-route-authenticate.t0000644000175100001660000000333014766301334024426 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; my $auth = app->routes->under('/api')->to( cb => sub { my $c = shift; my $spec = $c->openapi->spec; # skip authentication return 1 if $spec->{'x-no-auth'}; # really bad authentication return 1 if $c->param('unsafe_token'); # not authenticated $c->render(openapi => {errors => [{message => 'not logged in'}]}, status => 401); return; } ); get '/login' => sub { shift->render(openapi => {id => 123}, status => 200) }, 'login'; get '/protected' => sub { shift->render(openapi => {protected => 'secret'}, status => 200) }, 'protected'; plugin OpenAPI => {route => $auth, url => 'data://main/api.json'}; my $t = Test::Mojo->new; $t->get_ok('/api')->status_is(401)->json_is('/errors/0/message', 'not logged in'); $t->get_ok('/api?unsafe_token=1')->status_is(200)->json_is('/swagger', '2.0'); $t->get_ok('/api/login')->status_is(200)->json_is('/id', 123); $t->get_ok('/api/protected')->status_is(401)->json_is('/errors/0/message', 'not logged in'); $t->get_ok('/api/protected?unsafe_token=1')->status_is(200)->json_is('/protected', 'secret'); done_testing; __DATA__ @@ api.json { "swagger": "2.0", "info": { "version": "0.8", "title": "Test protected api" }, "basePath": "/api", "paths": { "/login": { "get": { "x-no-auth": true, "x-mojo-name": "login", "responses": { "200": { "description": "response", "schema": { "type": "object" } } } } }, "/protected": { "get": { "x-mojo-name": "protected", "responses": { "200": { "description": "response", "schema": { "type": "object" } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/v2-headers.t0000644000175100001660000000566314766301334020375 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; my $what_ever; get '/headers' => sub { my $c = shift; my $x_array_before = $c->req->headers->header('x-array'); return unless $c->openapi->valid_input; my $args = $c->validation->output; $c->res->headers->header('what-ever' => ref $what_ever ? @$what_ever : $what_ever); $c->res->headers->header('x-bool' => $args->{'x-bool'}) if exists $args->{'x-bool'}; $c->render( openapi => {args => $args, x_array => [$x_array_before, $c->req->headers->header('x-array')]}); }, 'dummy'; plugin OpenAPI => {url => 'data://main/headers.json'}; my $t = Test::Mojo->new; $t->get_ok('/api/headers' => {'x-number' => 'x', 'x-string' => '123'})->status_is(400) ->json_is('/errors/0', {'path' => '/x-number', 'message' => 'Expected number - got string.'}); $what_ever = '123'; $t->get_ok('/api/headers' => {'x-number' => 42.3, 'x-string' => '123'})->status_is(200) ->header_is('what-ever', '123') ->json_is('', {args => {'x-number' => 42.3, 'x-string' => 123}, x_array => [undef, undef]}); # header() returns join(', ', @$headers), resulting in "42, 24" instead of "42,24", # since Mojolicious::Plugin::OpenAPI turns 42,24 into an array. every_header() on the # other hand will return [42, 24]. See perldoc -m Mojo::Headers $what_ever = [qw(1 2 3)]; $t->get_ok('/api/headers' => {'x-array' => '42,24'})->status_is(200) ->header_is('what-ever', '1, 2, 3') ->json_is('', {args => {'x-array' => [42, 24]}, x_array => ['42,24', '42, 24']}); for my $bool (qw(true false 1 0)) { my $s = $bool =~ /true|1/ ? 'true' : 'false'; $what_ever = '123'; $t->get_ok('/api/headers' => {'x-bool' => $bool})->status_is(200)->content_like(qr{"x-bool":$s}) ->header_is('x-bool', $s); } done_testing; __DATA__ @@ headers.json { "swagger" : "2.0", "info" : { "version": "9.1", "title" : "Test API for body parameters" }, "consumes" : [ "application/json" ], "produces" : [ "application/json" ], "schemes" : [ "http" ], "basePath" : "/api", "paths" : { "/headers" : { "get" : { "x-mojo-name": "dummy", "parameters" : [ { "in": "header", "name": "x-bool", "type": "boolean", "description": "desc..." }, { "in": "header", "name": "x-number", "type": "number", "description": "desc..." }, { "in": "header", "name": "x-string", "type": "string", "description": "desc..." }, { "in": "header", "name": "x-array", "items": { "type": "string" }, "type": "array", "description": "desc..." } ], "responses" : { "200" : { "description": "this is required", "headers": { "x-bool": { "type": "boolean" }, "what-ever": { "type": "array", "items": { "type": "string" }, "minItems": 1 } }, "schema": { "type" : "object" } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/basic-mojo-placeholder.t0000644000175100001660000000341714766301334022733 0ustar runnerdockeruse Mojo::Base -strict; use Mojo::Util 'monkey_patch'; use Test::Mojo; use Test::More; my $t = Test::Mojo->new(make_app()); monkey_patch 'Myapp::Controller::Pet' => one => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {username => $c->stash('username')}); }; $t->app->plugin(OpenAPI => {url => 'data://main/echo.json'}); $t->get_ok('/api/jhthorsen@cpan.org')->status_is(200)->json_is('/username' => 'jhthorsen@cpan.org'); $t->options_ok('/api/jhthorsen@cpan.org?method=get')->status_is(200) ->json_is('/parameters/0/x-mojo-placeholder' => '#')->json_is('/parameters/0/in' => 'path') ->json_is('/parameters/0/name' => 'username')->json_is('/parameters/1/in' => 'query') ->json_is('/parameters/1/name' => 'fields')->json_hasnt('/x-all-parameters'); # make sure rendering doesn't croak when "parameters" are under a path # Not a HASH reference at template mojolicious/plugin/openapi/resource.html.ep $t->get_ok('/api.html')->status_is(200); done_testing; sub make_app { eval <<"HERE"; package Myapp; use Mojo::Base 'Mojolicious'; sub startup { } 1; package Myapp::Controller::Pet; use Mojo::Base 'Mojolicious::Controller'; 1; HERE return Myapp->new; } __DATA__ @@ echo.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Pets" }, "schemes" : [ "http" ], "basePath" : "/api", "paths" : { "/{username}" : { "parameters": [ { "x-mojo-placeholder": "#", "in": "path", "name": "username", "required": true, "type": "string" } ], "get" : { "x-mojo-to" : "pet#one", "parameters" : [ { "in": "query", "name": "fields", "type": "string" } ], "responses" : { "200": { "description": "Echo response", "schema": { "type": "object" } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/plugin-security-extended-status.t0000644000175100001660000000576714766301334024724 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; get '/securitytest' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {message => "ok"}, status => 200); }, 'securitytest'; plugin OpenAPI => { url => 'data://main/sec.yaml', security => { api_key => sub { my ($self, $definition, $scopes, $cb) = @_; my $apikey = $self->req->headers->header('apikey'); my $apiuser = $self->req->headers->header('apiuser'); return $self->$cb("apikey/apiuser missing") unless $apikey and $apiuser; if ($apikey eq "authenticated" and $apiuser eq "authenticated") { $self->stash(status => 403); return $self->$cb("Permission denied"); } if ($apikey eq "authorized" and $apiuser eq "authorized") { return $self->$cb(); } return $self->$cb("Unauthorized"); }, }, }; my $t = Test::Mojo->new; $t->get_ok('/api/securitytest' => {apikey => 'authorized', apiuser => 'authorized'}) ->status_is(200); $t->get_ok('/api/securitytest' => {apikey => 'authenticated', apiuser => 'authenticated'}) ->status_is(403); $t->get_ok('/api/securitytest' => {apikey => 'unknown', apiuser => 'unknown'})->status_is(401); $t->get_ok('/api/securitytest')->status_is(401); done_testing; __DATA__ @@ sec.yaml openapi: 3.0.2 info: title: CSApi version: "1.0" description: API test servers: - url: /api security: - api_key: [] api_user: [] paths: /securitytest: get: x-mojo-name: securitytest summary: test security description: > Will grant authenticated and authorized apikeys access responses: 200: $ref: "#/components/responses/200_OK_message" 403: $ref: "#/components/responses/403_Forbidden" components: securitySchemes: api_key: type: apiKey description: API key to authorize requests. name: apikey in: header api_user: type: apiKey description: Username going with that key. name: apiuser in: header schemas: Error: type: object required: - errors properties: errors: type: array items: type: object required: - message properties: message: type: string path: type: string responses: 200_OK_message: description: OK content: application/json: schema: type: object required: - message properties: message: type: string path: type: string 403_Forbidden: description: Insufficient priviliges content: application/json: schema: $ref: "#/components/schemas/Error" DefaultResponse: description: Default Response content: application/json: schema: $ref: "#/components/schemas/Error" Mojolicious-Plugin-OpenAPI-5.11/t/plugin-security-v3.t0000644000175100001660000003135314766301334022121 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; post '/global' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 1}); }, 'global'; post('/fail_escape' => sub { shift->render(openapi => {ok => 1}) }, 'fail_escape'); post '/simple' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 1}); }, 'simple'; options '/options' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 1}); }, 'options'; post '/fail_or_pass' => sub { my $c = shift->openapi->valid_input or return; die 'Could not connect to dummy database error message' if $ENV{DUMMY_DB_ERROR}; $c->render(openapi => {ok => 1}); }, 'fail_or_pass'; post '/fail_and_pass' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 1}); }, 'fail_and_pass'; post '/multiple_fail' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 1}); }, 'multiple_fail'; post '/multiple_and_fail' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 1}); }, 'multiple_and_fail'; post '/cache' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 1}); }, 'cache'; post '/die' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 1}); }, 'die'; our %checks; plugin OpenAPI => { url => 'data://main/sec.json', security => { pass1 => sub { my ($c, $def, $scopes, $cb) = @_; $checks{pass1}++; $c->$cb; }, pass2 => sub { my ($c, $def, $scopes, $cb) = @_; $checks{pass2}++; $c->$cb; }, fail1 => sub { my ($c, $def, $scopes, $cb) = @_; $checks{fail1}++; # This deferment causes multiple_and_fail to report # out of order unless order is carefully maintained Mojo::IOLoop->next_tick(sub { $c->$cb('Failed fail1') }); }, fail2 => sub { my ($c, $def, $scopes, $cb) = @_; $checks{fail2}++; my %res = %$def; $res{message} = 'Failed fail2'; $c->$cb(\%res); }, '~fail/escape' => sub { my ($c, $def, $scopes, $cb) = @_; $checks{'~fail/escape'}++; $c->$cb('Failed ~fail/escape'); }, die => sub { my ($c, $def, $scopes, $cb) = @_; $checks{die}++; die 'Argh!'; }, }, }; my %security_definition = (description => 'fail2', in => 'header', name => 'Authorization', type => 'apiKey'); my $t = Test::Mojo->new; subtest 'post global' => sub { local %checks; $t->post_ok('/api/global' => json => {})->status_is(200)->json_is('/ok' => 1); is_deeply \%checks, {pass1 => 1}, 'expected checks occurred'; }; subtest 'options global' => sub { # global does not define an options handler, so it gets the default # which is allowed through the security local %checks; $t->options_ok('/api/global')->status_is(200); is_deeply \%checks, {}, 'expected checks occurred'; }; subtest 'post simple' => sub { local %checks; $t->post_ok('/api/simple' => json => {})->status_is(200)->json_is('/ok' => 1); is_deeply \%checks, {pass2 => 1}, 'expected checks occurred'; }; subtest 'options options' => sub { # route defined with an options handler so it must use the defined security local %checks; $t->options_ok('/api/options' => json => {})->status_is(200)->json_is('/ok' => 1); is_deeply \%checks, {pass1 => 1}, 'expected checks occurred'; }; subtest 'post fail_or_pass' => sub { local %checks; $t->post_ok('/api/fail_or_pass' => json => {})->status_is(200)->json_is('/ok' => 1); is_deeply \%checks, {fail1 => 1, pass1 => 1}, 'expected checks occurred'; }; subtest 'post fail_or_pass - env' => sub { local $ENV{DUMMY_DB_ERROR} = 1; $t->post_ok('/api/fail_or_pass' => json => {})->status_is(500) ->json_is('/errors/0/message', 'Internal Server Error.')->json_is('/errors/0/path', '/'); }; subtest 'post fail_and_pass' => sub { local %checks; $t->post_ok('/api/fail_and_pass' => json => {})->status_is(401) ->json_is( {errors => [{message => 'Failed fail1', path => '/security/0/fail1'}], status => 401}); is_deeply \%checks, {fail1 => 1, pass1 => 1}, 'expected checks occurred'; }; subtest 'post multiple_fail' => sub { local %checks; $t->post_ok('/api/multiple_fail' => json => {})->status_is(401)->json_is({ status => 401, errors => [ {message => 'Failed fail1', path => '/security/0/fail1'}, {message => 'Failed fail2', %security_definition}, ] }); is_deeply \%checks, {fail1 => 1, fail2 => 1}, 'expected checks occurred'; }; subtest 'post multiple_and_fail' => sub { local %checks; $t->post_ok('/api/multiple_and_fail' => json => {})->status_is(401)->json_is({ status => 401, errors => [ {message => 'Failed fail1', path => '/security/0/fail1'}, {message => 'Failed fail2', %security_definition} ] }); is_deeply \%checks, {fail1 => 1, fail2 => 1}, 'expected checks occurred'; }; subtest 'post fail_escape' => sub { local %checks; $t->post_ok('/api/fail_escape' => json => {})->status_is(401)->json_is({ errors => [{message => 'Failed ~fail/escape', path => '/security/0/~0fail~1escape'}], status => 401 }); is_deeply \%checks, {'~fail/escape' => 1}, 'expected checks occurred'; }; subtest 'post cache' => sub { local %checks; $t->post_ok('/api/cache' => json => {})->status_is(200)->json_is('/ok' => 1); is_deeply \%checks, {fail1 => 1, pass1 => 1, pass2 => 1}, 'expected checks occurred'; }; subtest 'post die' => sub { local %checks; $t->post_ok('/api/die' => json => {})->status_is(500)->json_has('/errors/0/message'); is_deeply \%checks, {die => 1}, 'expected checks occurred'; }; done_testing; __DATA__ @@ sec.json { "openapi": "3.0.0", "info": { "version": "0.8", "title": "Pets" }, "servers": [ { "url": "http://petstore.swagger.io/api" } ], "components": { "responses": { "defaultResponse": { "description": "default response", "content": { "application/json": { "schema": { "type": "object", "properties": { "errors": { "type": "array", "items": { "type": "object", "properties": { "message": { "type": "string" }, "path": { "type": "string" } }, "required": ["message"] } } }, "required": ["errors"] } } } } }, "securitySchemes": { "pass1": { "type": "apiKey", "name": "Authorization", "in": "header", "description": "pass1" }, "pass2": { "type": "apiKey", "name": "Authorization", "in": "header", "description": "pass2" }, "fail1": { "type": "apiKey", "name": "Authorization", "in": "header", "description": "fail1" }, "fail2": { "type": "apiKey", "name": "Authorization", "in": "header", "description": "fail2" }, "~fail/escape": { "type": "apiKey", "name": "Authorization", "in": "header", "description": "dummy" }, "die": { "type": "apiKey", "name": "Authorization", "in": "header", "description": "die" } } }, "security": [{"pass1": []}], "paths": { "/global": { "post": { "x-mojo-name": "global", "requestBody": { "content": { "application/json": { "schema": { "type": "object" } } } }, "responses": { "200": {"description": "Echo response", "content": { "application/json": { "schema": { "type": "object" } } }} } } }, "/simple": { "post": { "x-mojo-name": "simple", "security": [{"pass2": []}], "requestBody": { "content": { "application/json": { "schema": { "type": "object" } } } }, "responses": { "200": {"description": "Echo response", "content": { "application/json": { "schema": { "type": "object" } } }} } } }, "/options": { "options": { "x-mojo-name": "options", "security": [{"pass1": []}], "requestBody": { "content": { "application/json": { "schema": { "type": "object" } } } }, "responses": { "200": {"description": "Echo response", "content": { "application/json": { "schema": { "type": "object" } } }} } } }, "/fail_or_pass": { "post": { "x-mojo-name": "fail_or_pass", "security": [ {"fail1": []}, {"pass1": []} ], "requestBody": { "content": { "application/json": { "schema": { "type": "object" } } } }, "responses": { "200": {"description": "Echo response", "content": { "application/json": { "schema": { "type": "object" } } }} } } }, "/fail_and_pass": { "post": { "x-mojo-name": "fail_and_pass", "security": [ { "fail1": [], "pass1": [] } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object" } } } }, "responses": { "200": {"description": "Echo response", "content": { "application/json": { "schema": { "type": "object" } } }} } } }, "/multiple_fail": { "post": { "x-mojo-name": "multiple_fail", "security": [ { "fail1": [] }, { "fail2": [] } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object" } } } }, "responses": { "200": {"description": "Echo response", "content": { "application/json": { "schema": { "type": "object" } } }} } } }, "/multiple_and_fail": { "post": { "x-mojo-name": "multiple_and_fail", "security": [ { "fail1": [], "fail2": [] } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object" } } } }, "responses": { "200": {"description": "Echo response", "content": { "application/json": { "schema": { "type": "object" } } }} } } }, "/fail_escape": { "post": { "x-mojo-name": "fail_escape", "security": [{"~fail/escape": []}], "requestBody": { "content": { "application/json": { "schema": { "type": "object" } } } }, "responses": { "200": {"description": "Echo response", "content": { "application/json": { "schema": { "type": "object" } } }} } } }, "/cache": { "post": { "x-mojo-name": "cache", "security": [ { "fail1": [], "pass1": [] }, { "pass1": [], "pass2": [] } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object" } } } }, "responses": { "200": {"description": "Echo response", "content": { "application/json": { "schema": { "type": "object" } } }} } } }, "/die": { "post": { "x-mojo-name": "die", "security": [ {"die": []}, {"pass1": []} ], "requestBody": { "content": { "application/json": { "schema": { "type": "object" } } } }, "responses": { "200": {"description": "Echo response", "content": { "application/json": { "schema": { "type": "object" } } }} } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/v2-tutorial.t0000644000175100001660000000415514766301334020620 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; make_app(); make_controller(); my $t = Test::Mojo->new('Myapp'); $t->get_ok('/api')->status_is(200)->json_is('/info/title', 'Some awesome API'); $t->get_ok('/api/pets')->status_is(200)->json_is('/pets/0/name', 'kit-e-cat'); done_testing; sub make_app { eval <<'HERE' or die $@; package Myapp; use Mojo::Base "Mojolicious"; sub startup { my $app = shift; $app->plugin("OpenAPI" => {url => "data://main/myapi.json"}); } $ENV{"Myapp.pm"} = 1; HERE } sub make_controller { eval <<'HERE' or die $@; package Myapp::Controller::Pet; use Mojo::Base "Mojolicious::Controller"; sub list { # Do not continue on invalid input and render a default 400 # error document. my $c = shift->openapi->valid_input or return; # $c->openapi->valid_input copies valid data to validation object, # and the normal Mojolicious api works as well. my $input = $c->validation->output; my $age = $c->param("age"); # same as $input->{age} my $body = $c->req->json; # same as $input->{body} # $output will be validated by the OpenAPI spec before rendered my $output = {pets => [{name => "kit-e-cat"}]}; $c->render(openapi => $output); } $ENV{"Myapp/Controller/Pet.pm"} = 1; HERE } __DATA__ @@ myapi.json { "swagger": "2.0", "info": { "version": "1.0", "title": "Some awesome API" }, "basePath": "/api", "paths": { "/pets": { "get": { "operationId": "getPets", "x-mojo-name": "get_pets", "x-mojo-to": "pet#list", "summary": "Finds pets in the system", "parameters": [ {"in": "body", "name": "body", "schema": {"type": "object"}}, {"in": "query", "name": "age", "type": "integer"} ], "responses": { "200": { "description": "Pet response", "schema": { "type": "object", "properties": { "pets": { "type": "array", "items": { "type": "object" } } } } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/basic-empty-response.t0000644000175100001660000000215614766301334022500 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; my $t = Test::Mojo->new; my ($res, $status); get '/string' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $res, status => $status); }, 'File'; plugin OpenAPI => {url => 'data://main/file.json'}; ($res, $status) = ('', 200); $t->get_ok('/api/string')->status_is(200)->content_is('""'); ($res, $status) = (undef, 200); $t->get_ok('/api/string')->status_is(200)->content_is('null'); ($res, $status) = ('', 204); $t->get_ok('/api/string')->status_is(204)->content_is(''); done_testing; package main; __DATA__ @@ file.json { "swagger": "2.0", "info": {"version": "0.8", "title": "Test empty response"}, "schemes": ["http"], "basePath": "/api", "paths": { "/string": { "get": { "operationId": "File", "responses": { "200": { "description": "response", "schema": {"type": ["null", "string"]} }, "204": { "description": "empty", "schema": {"type": ["string"]} } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/v2-id-prop.t0000644000175100001660000000226214766301334020324 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; my $id = 'foo'; get '/user' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {email => 'jhthorsen@cpan.org', id => $id}); }, 'getUser'; plugin OpenAPI => {url => 'data://main/schema.json'}; my $t = Test::Mojo->new; $t->get_ok('/api/user')->status_is(500) ->json_is('/errors/0', {message => 'Expected integer - got string.', path => '/body/id'}); $id = 42; $t->get_ok('/api/user')->status_is(200)->json_is('/email', 'jhthorsen@cpan.org') ->json_is('/id', 42); done_testing; __DATA__ @@ schema.json { "swagger": "2.0", "info": { "version": "0.8", "title": "Test" }, "basePath": "/api", "paths": { "/user": { "get": { "operationId": "getUser", "responses": { "200": { "description": "ok", "examples": { "application/json": {"id": "42"} }, "schema": { "type": "object", "properties": { "email": {"type": "string"}, "id": {"type": "integer"} } } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/v2-validate-schema.t0000644000175100001660000000260714766301334022004 0ustar runnerdockeruse Mojo::Base -strict; use Test::More; use Mojolicious::Lite; eval { plugin OpenAPI => {url => 'data://main/invalid.json'} }; like $@, qr{/info: Missing property}si, 'missing spec elements'; eval { plugin OpenAPI => {url => 'data://main/swagger2/issues/89.json'} }; like $@, qr{/definitions/\$ref}si, 'ref in the wrong place'; eval { plugin OpenAPI => {skip_validating_specification => 1, url => 'data://main/invalid.json'} }; ok !$@, 'skip_validating_specification=1' or diag $@; done_testing; __DATA__ @@ invalid.json { "swagger" : "2.0", "paths" : {} } @@ swagger2/issues/89.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Test auto response" }, "paths" : { "$ref": "#/x-def/paths" }, "definitions": { "$ref": "#/x-def/defs" }, "x-responses": { "with_ref": { "post": {"$ref": "#/x-responses/with_get_ref"} }, "with_get_ref": { "responses": { "201": { "description": "response", "schema": { "type": "object" } } } } }, "x-def": { "defs": { "foo": { "properties": {} } }, "paths": { "/with-ref": {"$ref": "#/x-responses/with_ref"}, "/with-get-ref": { "get": {"$ref": "#/x-responses/with_get_ref"} }, "/auto" : { "post" : { "responses" : { "200": { "description": "response", "schema": { "type": "object" } } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/basic-404-501.t0000644000175100001660000000437314766301334020323 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; package MyApp; use Mojo::Base 'Mojolicious'; sub startup { my $app = shift; $app->plugin(OpenAPI => {url => 'data://main/v2.yaml'}); $app->plugin(OpenAPI => {url => 'data://main/v3.yaml'}); } package MyApp::Controller::User; use Mojo::Base 'Mojolicious::Controller'; sub find { my $c = shift->openapi->valid_input or return; $c->render(openapi => {age => 42}, status => $c->param('code') || 200); } package main; my $t = Test::Mojo->new(MyApp->new); for my $base_url (qw(/v2 /v3)) { subtest $base_url => sub { $t->get_ok("$base_url/user")->status_is(200)->json_is('/age', 42); $t->get_ok("$base_url/user?code=201")->status_is(501) ->json_is('/errors/0/message', 'No response rule for "201".'); $t->get_ok("$base_url/user/foo")->status_is(404)->json_is('/errors/0/message', 'Not Found.'); }; } done_testing; __DATA__ @@ v2.yaml swagger: "2.0" info: {version: "0.8", title: v2} basePath: /v2 paths: /user: delete: x-mojo-to: 'user#delete' responses: 200: description: 'TODO' schema: {type: object} get: x-mojo-to: 'user#find' responses: 200: description: User schema: type: object properties: age: {type: integer} post: x-mojo-to: 'user#create' parameters: - {in: formData, name: age, type: integer} responses: 400: description: Error schema: {type: object} @@ v3.yaml openapi: 3.0.0 info: {version: '0.8', title: v2} servers: - url: http://petstore.swagger.io/v3 paths: /user: delete: x-mojo-to: 'user#delete' responses: 200: description: 'TODO' content: application/json: schema: {type: object} get: x-mojo-to: 'user#find' responses: 200: description: User content: application/json: schema: type: object properties: age: {type: integer} post: x-mojo-to: 'user#create' responses: 400: description: Error content: application/json: schema: {type: object} Mojolicious-Plugin-OpenAPI-5.11/t/v3-valid_file_refs.t0000644000175100001660000000113014766301334022061 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; get '/test' => sub { my $c = shift->openapi->valid_input or return; $c->render(status => 200, openapi => $c->param('pcversion')); }, 'File'; plugin OpenAPI => {url => app->home->rel_file('spec/v3-valid_file_refs.yaml')}; my $t = Test::Mojo->new; $t->get_ok('/api')->status_is(200)->json_is('/components/parameters/PCVersion/name', 'pcversion'); my $validator = JSON::Validator::Schema::OpenAPIv3->new($t->get_ok('/api')->tx->res->body); is $validator->errors->[0], undef, 'valid bundled spec'; done_testing; Mojolicious-Plugin-OpenAPI-5.11/t/v2-collectionformat.t0000644000175100001660000001634514766301334022325 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; get '/header' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'getHeader'; get '/pets' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'getPets'; post '/pets' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'postPets'; get '/pets/:id' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'getPetsById'; plugin OpenAPI => {url => 'data://main/discriminator.json'}; my $t = Test::Mojo->new; subtest 'Expected array - got null' => sub { $t->get_ok('/api/pets')->status_is(400)->json_is('/errors/0/path', '/ri'); }; subtest 'Expected integer - got number.' => sub { $t->get_ok('/api/pets?ri=1.3')->status_is(400)->json_is('/errors/0/path', '/ri/0'); }; subtest 'Not enough items: 1\/2' => sub { $t->get_ok('/api/pets?ri=3&ml=5')->status_is(400)->json_is('/errors/0/path', '/ml'); }; subtest 'Valid' => sub { $t->get_ok('/api/pets?ri=3&ml=4&ml=2')->status_is(200)->json_is('/ml', [4, 2]) ->json_is('/ri', [3]); }; subtest 'In path' => sub { $t->get_ok('/api/pets/ilm,a,r,i')->status_is(200)->json_is('/id', [qw(ilm a r i)]); }; subtest 'In query' => sub { $t->post_ok('/api/pets?idq=ilm,a,r,i')->status_is(200)->json_is('/idq', [qw(ilm a r i)]); $t->post_ok("/api/pets?idq-tsv=ilm\ta\tr\ti")->status_is(200) ->json_is('/idq-tsv', [qw(ilm a r i)]); $t->post_ok('/api/pets?idq-ssv=ilm a r i')->status_is(200)->json_is('/idq-ssv', [qw(ilm a r i)]); $t->post_ok('/api/pets?idq-pipes=ilm|a|r|i')->status_is(200) ->json_is('/idq-pipes', [qw(ilm a r i)]); }; subtest 'In formData' => sub { $t->post_ok('/api/pets' => form => {idf => 'ilm,a,r,i'})->status_is(200) ->json_is('/idf', [qw(ilm a r i)]); $t->post_ok('/api/pets' => form => {'idf-tsv' => "ilm\ta\tr\ti"})->status_is(200) ->json_is('/idf-tsv', [qw(ilm a r i)]); $t->post_ok('/api/pets' => form => {'idf-ssv' => 'ilm a r i'})->status_is(200) ->json_is('/idf-ssv', [qw(ilm a r i)]); $t->post_ok('/api/pets' => form => {'idf-pipes' => 'ilm|a|r|i', 'a' => 'b'})->status_is(200) ->json_is('/idf-pipes', [qw(ilm a r i)]); $t->post_ok('/api/pets' => {'Content-Type' => 'application/x-www-form-urlencoded'} => 'idf-multi=ilm&idf-multi=a')->status_is(200)->json_is('/idf-multi', [qw(ilm a)]); }; subtest 'In header' => sub { $t->get_ok('/api/header')->status_is(200)->content_is('{}'); $t->get_ok('/api/header', {'X-Collection' => ''})->status_is(200)->json_is('/X-Collection' => []); $t->get_ok('/api/header', {'X-Collection' => 'a,b'})->status_is(200) ->json_is('/X-Collection' => [qw(a b)]); }; done_testing; __DATA__ @@ discriminator.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Test collectionFormat" }, "basePath": "/api", "paths" : { "/header" : { "get" : { "operationId" : "getHeader", "parameters" : [ { "name":"X-Collection", "in":"header", "type":"array", "collectionFormat":"csv", "items":{"type":"string"}, "minItems":0 } ], "responses" : { "200": { "description": "response", "schema": { "type": "object" } } } } }, "/pets/{id}" : { "get" : { "operationId" : "getPetsById", "parameters" : [ { "name":"id", "in":"path", "type":"array", "collectionFormat":"csv", "items":{"type":"string"}, "minItems":0, "required":true } ], "responses" : { "200": { "description": "pet response", "schema": { "type": "object" } } } } }, "/pets" : { "get" : { "operationId" : "getPets", "parameters" : [ { "name":"no", "in":"query", "type":"array", "collectionFormat":"multi", "items":{"type":"integer"}, "minItems":0 }, { "name":"ml", "in":"query", "type":"array", "collectionFormat":"multi", "items":{"type":"integer"}, "minItems":2 }, { "name":"ri", "in":"query", "type":"array", "collectionFormat":"multi", "required":true, "items":{"type":"integer"}, "minItems":1 } ], "responses" : { "200": { "description": "pet response", "schema": { "type": "object" } } } }, "post" : { "operationId" : "postPets", "parameters" : [ { "name":"idq", "in":"query", "type":"array", "collectionFormat":"csv", "items":{"type":"string"}, "minItems":0, "required":false }, { "name":"idq-tsv", "in":"query", "type":"array", "collectionFormat":"tsv", "items":{"type":"string"}, "minItems":0, "required":false }, { "name":"idq-ssv", "in":"query", "type":"array", "collectionFormat":"ssv", "items":{"type":"string"}, "minItems":0, "required":false }, { "name":"idq-pipes", "in":"query", "type":"array", "collectionFormat":"pipes", "items":{"type":"string"}, "minItems":0, "required":false }, { "name":"idf", "in":"formData", "type":"array", "collectionFormat":"csv", "items":{"type":"string"}, "minItems":0, "required":false }, { "name":"idf-tsv", "in":"formData", "type":"array", "collectionFormat":"tsv", "items":{"type":"string"}, "minItems":0, "required":false }, { "name":"idf-ssv", "in":"formData", "type":"array", "collectionFormat":"ssv", "items":{"type":"string"}, "minItems":0, "required":false }, { "name":"idf-pipes", "in":"formData", "type":"array", "collectionFormat":"pipes", "items":{"type":"string"}, "minItems":0, "required":false }, { "name":"idf-multi", "in":"formData", "type":"array", "collectionFormat":"multi", "items":{"type":"string"}, "minItems":0, "required":false } ], "responses" : { "200": { "description": "pet response", "schema": { "type": "object" } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/00-project.t0000644000175100001660000000317314766301334020312 0ustar runnerdockeruse Test::More; use File::Find; plan skip_all => 'No such directory: .git' unless $ENV{TEST_ALL} or -d '.git'; plan skip_all => 'HARNESS_PERL_SWITCHES =~ /Devel::Cover/' if +($ENV{HARNESS_PERL_SWITCHES} || '') =~ /Devel::Cover/; for (qw( Test::CPAN::Changes::changes_file_ok+VERSION!4 Test::Pod::Coverage::pod_coverage_ok+VERSION!1 Test::Pod::pod_file_ok+VERSION!1 Test::Spelling::pod_file_spelling_ok+has_working_spellchecker!1 )) { my ($fqn, $module, $sub, $check, $skip_n) = /^((.*)::(\w+))\+(\w+)!(\d+)$/; next if eval "use $module;$module->$check"; no strict qw(refs); *$fqn = sub { SKIP: { skip "$sub(@_) ($module is required)", $skip_n } }; } my @files; find({wanted => sub { /\.pm$/ and push @files, $File::Find::name }, no_chdir => 1}, -e 'blib' ? 'blib' : 'lib'); plan tests => @files * 4 + 4; Test::Spelling::add_stopwords() if Test::Spelling->can('has_working_spellchecker') && Test::Spelling->has_working_spellchecker; for my $file (@files) { my $module = $file; $module =~ s,\.pm$,,; $module =~ s,.*/?lib/,,; $module =~ s,/,::,g; ok eval "use $module; 1", "use $module" or diag $@; Test::Pod::pod_file_ok($file); Test::Pod::Coverage::pod_coverage_ok($module, {also_private => [qr/^[A-Z_]+$/]}); Test::Spelling::pod_file_spelling_ok($file); } Test::CPAN::Changes::changes_file_ok(); __DATA__ Anwar Bernhard Berov CORS Gim Graf Hege Henning Hradek Hyeon Ji Krasimir Kristensen Linn Lund Mojolicious Morrott Oauth OpenAPI OpenAPIv Preflighted RENDERER Rassadin Renvoize SebMourlhou Søren Thegler Thorsen basePath html mojo openapi preflight preflighted renderer securityDefinitions validator Mojolicious-Plugin-OpenAPI-5.11/t/v2-basic.t0000644000175100001660000000211714766301334020032 0ustar runnerdockeruse Mojo::Base -strict; use Mojo::File 'path'; use Test::Mojo; use Test::More; use Mojolicious::Lite; get '/pets/:petId' => sub { my $c = shift->openapi->valid_input or return; my $input = $c->validation->output; my $output = {id => $input->{petId}, name => 'Cow'}; $output->{age} = 6 if $input->{wantAge}; $c->render(openapi => $output); }, 'showPetById'; get '/pets' => sub { my $c = shift->openapi->valid_input or return; $c->res->headers->header('x-next' => $c->param('limit') // 0); $c->render(openapi => $c->param('limit') ? [] : {}); }, 'listPets'; plugin OpenAPI => {url => path(__FILE__)->dirname->child(qw(spec v2-petstore.json))}; my $t = Test::Mojo->new; $t->get_ok('/v1.json')->status_is(200)->json_has('/basePath'); $t->get_ok('/v1/pets?limit=invalid', {Accept => 'application/json'})->status_is(400) ->json_is('/errors/0/path', '/limit') ->json_is('/errors/0/message', 'Expected integer - got string.'); $t->get_ok('/v1/pets?limit=5', {Accept => 'application/json'})->status_is(200) ->header_is('x-next', 5)->content_is('[]'); done_testing; Mojolicious-Plugin-OpenAPI-5.11/t/basic-register-plugin.t0000644000175100001660000001710514766301334022626 0ustar runnerdockeruse Mojo::Base -strict; use Mojo::JSON 'true'; use Test::Mojo; use Test::More; use Mojolicious::Lite; get( '/no-default-options/:id' => sub { $_[0]->render(openapi => {id => $_[0]->stash('id')}) }, 'Dummy' ); options( '/perl/no-default-options/:id' => sub { $_[0]->render(json => {options => $_[0]->stash('id')}) }); post('/user' => sub { shift->render(openapi => {}) }, 'User'); my $obj = plugin OpenAPI => {route => app->routes->any('/one'), url => 'data://main/one.json'}; plugin OpenAPI => {default_response_name => 'DefErr', url => 'data://main/two.json'}; plugin OpenAPI => { default_response_codes => [], spec => { swagger => '2.0', info => {version => '0.8', title => 'Test schema in perl'}, schemes => ['http'], basePath => '/perl', paths => { '/no-default-options/{id}' => { get => { operationId => 'Dummy', parameters => [{in => 'path', name => 'id', type => 'string', required => true}], responses => {200 => {description => 'response', schema => {type => 'object'}}} } }, '/user' => { post => { operationId => 'User', responses => {200 => {description => 'response', schema => {type => 'object'}}} } } } } }; plugin OpenAPI => { spec => { openapi => '3.0.0', info => { title => 'Sample API', description => 'Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.', version => '0.1.9' }, servers => [ { url => 'http://api.example.com/oa3', description => 'Optional server description, e.g. Main (production) server' }, { url => 'http://staging-api.example.com', description => 'Optional server description, e.g. Internal staging server for testing' } ], components => {schemas => {jobs => {type => 'array', items => {type => 'string'}}}}, paths => { '/users' => { get => { summary => 'Returns a list of users.', description => 'Optional extended description in CommonMark or HTML.', responses => { '200' => { description => 'A JSON array of user names', content => {'application/json' => {schema => {type => 'array', items => {type => 'string'}}}} } } } }, '/jobs' => { get => { summary => 'Returns a list of jobs.', description => 'Optional extended description in CommonMark or HTML.', responses => { '200' => { description => 'A JSON array of job types', content => {'application/json' => {schema => {'$ref' => '#/components/schemas/jobs'}}} } } } } } } }; plugin OpenAPI => {url => 'data://main/programmatically.json', op_spec_to_route => \&op_spec_to_route}; get('/injected' => {text => 'injected'}, 'injected'); my $schema = JSON::Validator::Schema::OpenAPIv2->new('data://main/schema-object.json'); $schema->data->{paths}{'/injected'} = { get => { operationId => 'injected', responses => {200 => {description => 'response', schema => {type => 'array'}}}, } }; plugin OpenAPI => {spec => $schema}; ok $obj->route->find('cool_api'), 'found api endpoint'; isa_ok($obj->route, 'Mojolicious::Routes::Route'); isa_ok($obj->validator, 'JSON::Validator::Schema::OpenAPIv2'); my $t = Test::Mojo->new; $t->get_ok('/one')->status_is(200) ->json_is('/definitions/DefaultResponse/properties/errors/type', 'array') ->json_is('/info/title', 'Test schema one'); $t->options_ok('/oa3/users?method=get')->status_is(200) ->json_is('/responses/200/description', 'A JSON array of user names') ->json_is('/responses/400/description', 'Default response.') ->json_is('/responses/400/content/application~1json/schema/$ref', '#/components/schemas/DefaultResponse'); $t->options_ok('/oa3/jobs?method=get')->status_is(200) ->json_is('/responses/200/description', 'A JSON array of job types') ->json_is('/responses/400/description', 'Default response.') ->json_is('/responses/200/content/application~1json/schema/$ref', '#/components/schemas/jobs'); $t->options_ok('/one/user?method=post')->status_is(200) ->json_is('/responses/200/description', 'ok') ->json_is('/responses/400/description', 'Default response.') ->json_is('/responses/400/schema/$ref', '#/definitions/DefaultResponse') ->json_is('/responses/500/description', 'err'); $t->get_ok('/two')->status_is(200)->json_is('/definitions/DefaultResponse', undef) ->json_is('/definitions/DefErr/required', [qw(errors something_else)]) ->json_is('/info/title', 'Test schema two'); $t->options_ok('/two/user?method=post')->status_is(200) ->json_is('/responses/400/schema/$ref', '#/definitions/DefErr') ->json_is('/responses/default/description', 'whatever'); $t->get_ok('/perl')->status_is(200)->json_is('/info/title', 'Test schema in perl'); $t->options_ok('/perl/user?method=post')->status_is(200) ->json_is('/responses/500/description', undef); note 'Override options'; $t->get_ok('/perl/no-default-options/42')->status_is(200)->json_is('/id', 42); $t->options_ok('/perl/no-default-options/42')->status_is(200)->json_is('/options', 42); note 'programmatically'; $t->get_ok('/api/programmatically')->status_is(200)->json_is('/operationId', 'getStuff') ->json_is('/responses/200/schema/type', 'array'); $t->post_ok('/api/programmatically')->status_is(200)->json_is('/operationId', 'postStuff') ->json_is('/responses/200/schema/type', 'boolean'); $t->get_ok('/schema-object/injected')->status_is(200)->content_is('injected'); done_testing; sub op_spec_to_route { my ($plugin, $op_spec, $route) = @_; $route->to(cb => sub { shift->render(json => $op_spec) }); } __DATA__ @@ one.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Test schema one" }, "schemes" : [ "http" ], "basePath" : "/api", "x-mojo-name": "cool_api", "paths" : { "/user" : { "post" : { "operationId" : "User", "responses" : { "200": { "description": "ok", "schema": { "type": "object" } }, "500": { "description": "err", "schema": { "type": "object" } } } } } } } @@ two.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Test schema two" }, "schemes" : [ "http" ], "basePath" : "/two", "paths" : { "/user" : { "post" : { "operationId" : "User", "responses" : { "200": { "description": "response", "schema": { "type": "object" } }, "default": { "description": "whatever", "schema": { "type": "array" } } } } } }, "definitions": { "DefErr": { "type": "object", "required": ["errors", "something_else"] } } } @@ schema-object.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Object" }, "basePath" : "/schema-object", "paths" : {} } @@ programmatically.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Test unique route names" }, "basePath" : "/api", "paths" : { "/programmatically" : { "get" : { "operationId" : "getStuff", "responses": { "200": { "description": "response", "schema": { "type": "array" } } } }, "post" : { "operationId" : "postStuff", "responses": { "200": { "description": "response", "schema": { "type": "boolean" } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/basic-mojo-route-names.t0000644000175100001660000000621014766301334022702 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; app->routes->namespaces(['MyApp::Controller']); get '/whatever' => sub { die 'Oh noes!' }, 'Whatever'; plugin OpenAPI => {url => 'data://main/lite.json'}; my $t = Test::Mojo->new; my $r = $t->app->routes; ok $r->find('Whatever'), 'Whatever is defined'; { local $TODO = 'This default route name might change in the future'; ok $r->find('my_api.whatever_options'), 'my_api.whatever_options is defined'; } eval { plugin OpenAPI => {url => 'data://main/unique-route.json'} }; like $@, qr{Route name "Whatever" is not unique}, 'unique route names'; eval { plugin OpenAPI => {url => 'data://main/unique-op.json'} }; like $@, qr{operationId "Whatever" is not unique}, 'unique operationId'; $t = Test::Mojo->new(Mojolicious->new); $r = $t->app->routes->namespaces(['MyApp::Controller']); $t->app->plugin(OpenAPI => {spec_route_name => 'my_api', url => 'data://main/full.json'}); ok $r->lookup('my_api'), 'my_api is defined'; $r = $r->lookup('my_api')->parent; ok $r->find('my_api.Whatever'), 'my_api.Whatever is defined'; done_testing; sub define_controller { eval <<'HERE' or die; package MyApp::Controller::Dummy; use Mojo::Base 'Mojolicious::Controller'; sub whatever {} 1; HERE } package main; __DATA__ @@ full.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Test route names" }, "basePath" : "/api", "paths" : { "/whatever" : { "get" : { "operationId" : "Whatever", "x-mojo-to": "dummy#whatever", "responses" : { "200": { "description": "response", "schema": { "type": "object" } } } } }, "/no-endpoint": { "get" : { "operationId" : "NoEndpoint", "responses" : { "200": { "description": "response", "schema": { "type": "object" } } } } } } } @@ lite.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Test route names" }, "basePath" : "/api", "paths" : { "/whatever" : { "get" : { "operationId" : "Whatever", "responses" : { "200": { "description": "response", "schema": { "type": "object" } } } } } } } @@ unique-op.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Test unique operationId" }, "basePath" : "/api", "paths" : { "/r" : { "get" : { "operationId": "Whatever", "responses": { "200": { "description": "response", "schema": { "type": "object" } } } }, "post" : { "operationId": "Whatever", "responses": { "200": { "description": "response", "schema": { "type": "object" } } } } } } } @@ unique-route.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Test unique route names" }, "basePath" : "/api", "paths" : { "/r" : { "get" : { "x-mojo-name": "Whatever", "responses": { "200": { "description": "response", "schema": { "type": "object" } } } }, "post" : { "x-mojo-name": "Whatever", "responses": { "200": { "description": "response", "schema": { "type": "object" } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/v2-file.t0000644000175100001660000000212614766301334017670 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; post '/user' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {}); }, 'createUser'; plugin OpenAPI => {url => 'data://main/readonly.json'}; my $t = Test::Mojo->new; $t->post_ok('/api/user')->status_is(400) ->json_is('/errors/0', {message => 'Missing property.', path => '/image'}); my $image = Mojo::Asset::Memory->new->add_chunk('smileyface'); $t->post_ok('/api/user', form => {id => 1, image => {file => $image}})->status_is(200); done_testing; __DATA__ @@ readonly.json { "swagger": "2.0", "info": { "version": "0.8", "title": "Test readonly" }, "schemes": [ "http" ], "basePath": "/api", "paths": { "/user": { "post": { "operationId": "createUser", "parameters": [ {"name": "image", "in": "formData", "type": "file", "required": true}, {"name": "id", "in": "formData", "type": "string"} ], "responses": { "200": { "description": "ok", "schema": { "type": "object" } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/v2-body.t0000644000175100001660000000305614766301334017711 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; for ('string', 'array') { my ($path, $name) = ("/body-$_", 'body' . ucfirst); post $path => sub { warn $_[0]->req->body; my $c = shift->openapi->valid_input or return; $c->render(text => $c->req->body, status => 200); }, $name; } plugin OpenAPI => {url => 'data:///api.yml'}; my $t = Test::Mojo->new; $t->post_ok('/api/body-string', {'Content-Type' => 'text/plain'} => 'invalid_json')->status_is(200) ->content_is('invalid_json'); $t->post_ok('/api/body-array', json => [{cool => 'beans'}])->status_is(200) ->json_is('/0/cool', 'beans'); $t->post_ok('/api/body-array', json => ['str'])->status_is(400) ->json_is('/errors/0', {path => '/body/0', message => 'Expected object - got string.'}); done_testing; __DATA__ @@ api.yml { "swagger": "2.0", "info": {"version": "0.8", "title": "Raw data"}, "basePath": "/api", "paths": { "/body-string": { "post": { "x-mojo-name": "bodyString", "parameters": [ {"name": "echo", "in": "body", "schema": {"type": "string"}} ], "responses": { "200": {"description": "response", "schema": {"type": "string"}} } } }, "/body-array": { "post": { "x-mojo-name": "bodyArray", "parameters": [ {"name": "body", "in": "body", "schema": {"type": "array", "items": {"type": "object"}}} ], "responses": { "200": {"description": "response", "schema": {"type": "array"}} } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/basic-path-parameters.t0000644000175100001660000000212614766301334022600 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; post '/user/:id' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {id => $c->param('id')}); }, 'user'; plugin OpenAPI => {url => "data://main/path-parameters.json"}; my $t = Test::Mojo->new; $t->post_ok('/api/user/foo' => json => {})->status_is(400); $t->post_ok('/api/user/42a' => json => {})->status_is(400); $t->post_ok('/api/user/42' => json => {})->status_is(200)->json_is('/id', 42); done_testing; __DATA__ @@ path-parameters.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Path parameters" }, "schemes" : [ "http" ], "basePath" : "/api", "paths" : { "/user/{id}" : { "parameters" : [ { "in": "path", "name": "id", "type": "integer", "required": true } ], "post" : { "x-mojo-name" : "user", "responses" : { "200": { "description": "User response", "schema": { "type": "object" } }, "400": { "description": "Invalid input", "schema": { "type": "object" } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/plugin-spec-renderer-options.t0000644000175100001660000000714614766301334024156 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; get '/spec' => sub { my $c = shift->openapi->valid_input or return; $c->render(json => {info => $c->openapi->spec('/info'), op_spec => $c->openapi->spec}); }, 'Spec'; get('/user/:id' => sub { shift->render(openapi => {}) }, 'user'); plugin OpenAPI => {url => 'data://main/spec.json'}; my $t = Test::Mojo->new; $t->get_ok('/api')->status_is(200)->json_is('/swagger', '2.0') ->json_is('/definitions/DefaultResponse/properties/errors/items/properties/message/type', 'string')->json_is('/definitions/SpecResponse/type', 'object') ->json_is('/paths/~1spec/get/operationId', 'Spec'); $t->get_ok('/api/spec')->status_is(200) ->json_is('/op_spec/responses/200/description', 'Spec response.') ->json_is('/info/version', '0.8'); $t->get_ok('/api/user/1')->status_is(200)->content_is('{}'); $t->options_ok('/api/spec')->status_is(200) ->json_is('/$schema', 'http://json-schema.org/draft-04/schema#') ->json_is('/title', 'Test spec response')->json_is('/description', '') ->json_is('/get/operationId', 'Spec') ->json_is('/get/responses/200/schema/$ref', '#/definitions/SpecResponse') ->json_is('/definitions/DefaultResponse/properties/errors/items/properties/message/type', 'string')->json_is('/definitions/SpecResponse/type', 'object'); $t->options_ok('/api/spec?method=get')->status_is(200) ->json_is('/$schema', 'http://json-schema.org/draft-04/schema#') ->json_is('/title', 'Test spec response')->json_is('/description', '') ->json_is('/operationId', 'Spec')->json_is('/definitions/SpecResponse/type', 'object'); eval { JSON::Validator->new->load_and_validate_schema($t->tx->res->json); ok 1, 'api/spec return valid schema'; } or do { ok 0, "api/spec return valid schema: $@"; }; $t->options_ok('/api/spec?method=post')->status_is(404) ->json_is('/errors/0/message', 'No spec defined.'); $t->options_ok('/api/user/1')->status_is(200) ->json_is('/$schema', 'http://json-schema.org/draft-04/schema#') ->json_is('/title', 'Test spec response')->json_is('/get/operationId', 'user') ->json_is('/definitions/DefaultResponse/properties/errors/items/properties/message/type', 'string'); $t->get_ok('/api')->status_is(200)->json_is('/basePath', '/api'); $t->head_ok('/api')->status_is(200); $t->head_ok('/api/user/1')->status_is(200)->content_is(''); hook before_dispatch => sub { my $c = shift; $c->req->url->base->path('/whatever'); }; $t->get_ok('/api')->status_is(200)->json_is('/basePath', '/whatever/api'); done_testing; __DATA__ @@ spec.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Test spec response" }, "basePath" : "/api", "paths" : { "/spec" : { "get" : { "operationId" : "Spec", "parameters" : [ { "in": "body", "name": "body", "schema": { "type" : "object" } } ], "responses" : { "200": { "description": "Spec response.", "schema": { "$ref": "#/definitions/SpecResponse" } } } } }, "/user/{id}" : { "parameters" : [ { "in": "path", "name": "id", "type": "integer", "required": true } ], "get" : { "operationId" : "user", "responses" : { "200": { "description": "User response.", "schema": { "type": "object" } } } } } }, "definitions": { "Object": { "type": "object" }, "SpecResponse": { "type": "object", "properties": { "get": { "$ref": "#/definitions/Object" } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/basic-custom-formats.t0000644000175100001660000000270714766301334022473 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; my $what_ever; get '/custom-format' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'custom_format'; my $oap = plugin OpenAPI => {url => 'data://main/custom-format.json'}; $oap->validator->formats->{need_to_be_x} = sub { $_[0] eq 'x' ? undef : 'Not x.' }; my $t = Test::Mojo->new; $t->get_ok('/api/custom-format' => json => {str => 'x'})->status_is(200) ->content_like(qr{"str":"x"}); $t->get_ok('/api/custom-format' => json => {str => 'y'})->status_is(400) ->content_like(qr{"errors"}); done_testing; __DATA__ @@ custom-format.json { "swagger" : "2.0", "info" : { "version": "9.1", "title" : "Test API for custom formats" }, "consumes" : [ "application/json" ], "produces" : [ "application/json" ], "schemes" : [ "http" ], "basePath" : "/api", "paths" : { "/custom-format" : { "get" : { "x-mojo-name": "custom_format", "parameters" : [ {"in": "body", "name": "body", "schema": {"$ref": "Body"}} ], "responses" : { "200" : { "description": "this is required", "schema": { "type" : "object" } } } } } }, "definitions": { "Body": { "required": ["str"], "properties": { "str": { "type": "string", "format": "need_to_be_x" } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/spec/0000755000175100001660000000000014766301352017170 5ustar runnerdockerMojolicious-Plugin-OpenAPI-5.11/t/spec/bundlecheck.json0000644000175100001660000000200514766301334022327 0ustar runnerdocker{ "swagger": "2.0", "info": { "title": "t-app", "version": "0.1.0", "license": { "name": "Apache License, Version 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" } }, "basePath": "/api", "host": "localhost:3000", "consumes": [ "application/json" ], "produces": [ "application/json" ], "paths": { "/t": { "get": { "operationId": "listT", "x-mojo-to": "Controller::OpenAPI::T#list", "tags": [ "t" ], "responses": { "200": { "description": "Self sufficient", "schema": { "items": { "type": "string" }, "type": "array" } }, "default": { "$ref": "#/responses/error" } } } } }, "responses": { "error": { "description": "Self sufficient", "schema": { "type": "object", "required": [ "error" ], "additionalProperties": false, "properties": { "error": { "type": "string" } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/spec/swagger/0000755000175100001660000000000014766301352020627 5ustar runnerdockerMojolicious-Plugin-OpenAPI-5.11/t/spec/swagger/responses/0000755000175100001660000000000014766301352022650 5ustar runnerdockerMojolicious-Plugin-OpenAPI-5.11/t/spec/swagger/responses/ok.yaml0000644000175100001660000000002714766301334024144 0ustar runnerdocker--- timestamp: integer Mojolicious-Plugin-OpenAPI-5.11/t/spec/swagger/swagger.yaml0000644000175100001660000000060714766301334023155 0ustar runnerdocker--- swagger: "2.0" info: title: "Test spec" version: "1.2.3" host: localhost basePath: "/swagger" schemes: ["https"] paths: /external/ref: $ref: "./paths/ref.yaml" /external/schema: post: parameters: - in: body name: body schema: $ref: "./parameters/body.yaml" responses: 200: $ref: "./responses/ok.yaml" Mojolicious-Plugin-OpenAPI-5.11/t/spec/swagger/paths/0000755000175100001660000000000014766301352021746 5ustar runnerdockerMojolicious-Plugin-OpenAPI-5.11/t/spec/swagger/paths/ref.yaml0000644000175100001660000000014214766301334023403 0ustar runnerdockerget: responses: 200: description: "Ref response" schema: "../responses/ok.yaml" Mojolicious-Plugin-OpenAPI-5.11/t/spec/swagger/parameters/0000755000175100001660000000000014766301352022772 5ustar runnerdockerMojolicious-Plugin-OpenAPI-5.11/t/spec/swagger/parameters/body.yaml0000644000175100001660000000002114766301334024604 0ustar runnerdocker--- age: integer Mojolicious-Plugin-OpenAPI-5.11/t/spec/v3-invalid_file_refs_no_path.yaml0000644000175100001660000000052414766301334025557 0ustar runnerdockeropenapi: 3.0.0 info: title: Test file refs version: "1" servers: - url: /api paths: /test: get: operationId: File parameters: - $ref: "v3-valid_include.yaml#" responses: "200": description: thing content: "*/*": schema: type: stringMojolicious-Plugin-OpenAPI-5.11/t/spec/v3-invalid_file_refs.yaml0000644000175100001660000000054014766301334024045 0ustar runnerdockeropenapi: 3.0.0 info: title: Test file refs version: "1" servers: - url: /api paths: /test: get: operationId: File parameters: - $ref: "v3-invalid_include.yaml#/PCVersion" responses: "200": description: thing content: "*/*": schema: type: stringMojolicious-Plugin-OpenAPI-5.11/t/spec/v2-petstore.json0000644000175100001660000000562214766301334022262 0ustar runnerdocker{ "swagger": "2.0", "info": { "version": "1.0.0", "title": "Swagger Petstore", "contact": { "name": "OAI", "url": "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v2.0/json/petstore.json" }, "license": {"name": "MIT"} }, "host": "petstore.swagger.io", "basePath": "/v1", "schemes": ["http"], "consumes": ["application/json"], "produces": ["application/json"], "paths": { "/pets": { "get": { "summary": "List all pets", "operationId": "listPets", "tags": ["pets"], "parameters": [ { "name": "limit", "in": "query", "description": "How many items to return at one time (max 100)", "required": false, "type": "integer", "format": "int32" } ], "responses": { "200": { "description": "An paged array of pets", "headers": { "x-next": { "type": "string", "description": "A link to the next page of responses" } }, "schema": {"$ref": "#/definitions/Pets"} }, "default": { "description": "unexpected error", "schema": {"$ref": "#/definitions/Error"} } } }, "post": { "summary": "Create a pet", "operationId": "createPets", "tags": ["pets"], "responses": { "201": { "description": "Null response" }, "default": { "description": "unexpected error", "schema": {"$ref": "#/definitions/Error"} } } } }, "/pets/{petId}": { "get": { "summary": "Info for a specific pet", "operationId": "showPetById", "tags": ["pets"], "parameters": [ { "name": "petId", "in": "path", "required": true, "description": "The id of the pet to retrieve", "type": "string" } ], "responses": { "200": { "description": "Expected response to a valid request", "schema": {"$ref": "#/definitions/Pets"} }, "default": { "description": "unexpected error", "schema": {"$ref": "#/definitions/Error"} } } } } }, "definitions": { "Pet": { "required": ["id", "name"], "properties": { "id": {"type": "integer", "format": "int64"}, "name": {"type": "string"}, "tag": {"type": "string"} } }, "Pets": { "type": "array", "items": {"$ref": "#/definitions/Pet"} }, "Error": { "required": ["code", "message"], "properties": { "code": {"type": "integer", "format": "int32"}, "message": {"type": "string"} } } } } Mojolicious-Plugin-OpenAPI-5.11/t/spec/v3-invalid_include.yaml0000644000175100001660000000027414766301334023536 0ustar runnerdockerPCVersion: name: pcversion in: query description: version of commands which will run on backend schema: type: string enum: - 9.6.1 - 10.1.0 default: 10.1.0 Mojolicious-Plugin-OpenAPI-5.11/t/spec/v3-valid_file_refs.yaml0000644000175100001660000000056414766301334023524 0ustar runnerdockeropenapi: 3.0.0 info: title: Test file refs version: "1" servers: - url: /api paths: /test: get: operationId: File parameters: - $ref: "v3-valid_include.yaml#/components/parameters/PCVersion" responses: "200": description: thing content: "*/*": schema: type: stringMojolicious-Plugin-OpenAPI-5.11/t/spec/v3-petstore.json0000644000175100001660000001134614766301334022263 0ustar runnerdocker{ "openapi": "3.0.0", "info": { "license": { "name": "MIT" }, "title": "Swagger Petstore", "version": "1.0.0" }, "servers": [ { "url": "http://petstore.swagger.io/v1" } ], "paths": { "/pets/{petId}": { "get": { "operationId": "showPetById", "tags": [ "pets" ], "summary": "Info for a specific pet", "parameters": [ { "description": "The id of the pet to retrieve", "in": "path", "name": "petId", "required": true, "schema": { "type": "string" } }, { "description": "Indicates if the age is wanted in the response object", "in": "query", "name": "wantAge", "schema": { "type": "boolean" } } ], "responses": { "default": { "description": "unexpected error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "200": { "description": "Expected response to a valid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } } } } } } }, "/pets": { "get": { "operationId": "listPets", "summary": "List all pets", "tags": [ "pets" ], "parameters": [ { "description": "How many items to return at one time (max 100)", "in": "query", "name": "limit", "required": false, "schema": { "type": "integer", "format": "int32" } } ], "responses": { "200": { "description": "An paged array of pets", "headers": { "x-next": { "schema": { "type": "string" }, "description": "A link to the next page of responses" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pets" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/Pets" } } } }, "default": { "description": "unexpected error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "post": { "operationId": "createPets", "summary": "Create a pet", "tags": [ "pets" ], "parameters": [ { "description": "Turn on/off debug", "in": "cookie", "name": "debug", "schema": { "type": "integer", "enum": [0, 1] } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }, "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/Pet" } } } }, "responses": { "201": { "description": "Null response", "content": { "*/*": { "schema": { "type": "string" } } } }, "default": { "description": "unexpected error", "content": { "*/*": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "Pets": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } }, "Pet": { "required": [ "id", "name" ], "properties": { "tag": { "type": "string" }, "id": { "type": "integer", "format": "int64" }, "name": { "type": "string" }, "age": { "type": "integer" } } }, "Error": { "required": [ "code", "message" ], "properties": { "code": { "format": "int32", "type": "integer" }, "message": { "type": "string" } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/spec/v3-valid_include.yaml0000644000175100001660000000037614766301334023212 0ustar runnerdockercomponents: parameters: PCVersion: name: pcversion in: query description: version of commands which will run on backend schema: type: string enum: - 9.6.1 - 10.1.0 default: 10.1.0 Mojolicious-Plugin-OpenAPI-5.11/t/v3-invalid_file_refs_no_path.t0000644000175100001660000000131714766301334024127 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; get '/test' => sub { my $c = shift->openapi->valid_input or return; $c->render(status => 200, openapi => $c->param('pcversion')); }, 'File'; plugin OpenAPI => {url => app->home->rel_file('spec/v3-invalid_file_refs_no_path.yaml')}; my $t = Test::Mojo->new; $t->get_ok('/api')->status_is(200)->json_hasnt('/PCVersion/name')->json_has('/components/schemas') ->content_like(qr!v3-valid_include_yaml!); eval { die JSON::Validator::Schema::OpenAPIv3->new($t->get_ok('/api')->tx->res->json)->errors->[0] }; like $@, qr/Properties not allowed: components/, 'load_and_validate_schema fails, wrong placement of data'; done_testing; Mojolicious-Plugin-OpenAPI-5.11/t/basic-legacy-swagger2.t0000644000175100001660000000246614766301334022475 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; post '/echo' => sub { my ($c, $data, $cb) = @_; $c->$cb({body => $data->{body}}, 200); }, 'echo'; get '/' => {text => 'test123'}; plugin OpenAPI => {url => 'data://main/echo.json'}; my $t = Test::Mojo->new; hook around_action => sub { my ($next, $c, $action, $last) = @_; return $next->() unless $last; return $next->() unless $c->openapi->spec; return unless $c->openapi->valid_input; my $cb = sub { my ($c, $data, $code) = @_; $c->render(openapi => $data, status => $code); }; return $c->$action($c->validation->output, $cb); }; $t->get_ok('/')->status_is(200)->content_is('test123'); $t->post_ok('/api/echo' => json => {foo => 123})->status_is(200)->json_is('/body/foo' => 123); done_testing; __DATA__ @@ echo.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Pets" }, "schemes" : [ "http" ], "basePath" : "/api", "paths" : { "/echo" : { "post" : { "x-mojo-name" : "echo", "parameters" : [ { "in": "body", "name": "body", "schema": { "type" : "object" } } ], "responses" : { "200": { "description": "Echo response", "schema": { "type": "object" } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/v3-defaults.t0000644000175100001660000000200714766301334020557 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; get '/test' => sub { my $c = shift->openapi->valid_input or return; $c->render(status => 200, openapi => $c->param('pcversion')); }, 'File'; plugin OpenAPI => {url => 'data://main/file.yaml'}; my $t = Test::Mojo->new; $t->get_ok('/api/test')->status_is(200)->content_is('"10.1.0"'); done_testing; package main; __DATA__ @@ file.yaml openapi: 3.0.0 info: title: Test defaults version: "1" servers: - url: /api paths: /test: get: operationId: File parameters: - $ref: "#/components/parameters/PCVersion" responses: "200": description: thing content: "*/*": schema: type: string components: parameters: PCVersion: name: pcversion in: query description: version of commands which will run on backend schema: type: string enum: - 9.6.1 - 10.1.0 default: 10.1.0 Mojolicious-Plugin-OpenAPI-5.11/t/v3-bundle.t0000644000175100001660000001000614766301334020217 0ustar runnerdockeruse Mojo::Base -strict; use Mojo::File 'path'; use Test::Mojo; use Test::More; use Mojolicious::Lite; get '/pets' => sub { my $c = shift->openapi->valid_input or return; $c->res->headers->header('x-next' => $c->param('limit') // 0); $c->render(openapi => $c->param('limit') ? [] : {}); }, 'listPets'; plugin OpenAPI => {url => 'data:///spec.json'}; my $t = Test::Mojo->new; $t->get_ok('/v1.json')->status_is(200)->json_is('/openapi' => '3.0.0') ->json_is('/info/title' => 'Swagger Petstore')->json_like('/servers/0/url' => qr{^http://.*/v1$}) ->json_is('/security/0/pass1', [])->json_is('/components/securitySchemes/apiKey/type' => 'http') ->json_is('/components/schemas/DefaultResponse/properties/errors/items/properties/message/type', 'string')->json_is('/components/schemas/Pet/required/0', 'id') ->json_is('/components/schemas/Pets/type', 'array') ->json_is('/paths/~1pets~1{petId}/get/parameters/0/schema/type', 'string') ->json_is('/paths/~1pets~1{petId}/get/responses/500/content/application~1json/schema/$ref', '#/components/schemas/DefaultResponse') ->json_hasnt('/paths/~1pets~1{petId}/get/parameters/0/type')->json_hasnt('/basePath'); done_testing; __DATA__ @@ spec.json { "openapi": "3.0.0", "info": { "license": { "name": "MIT" }, "title": "Swagger Petstore", "version": "1.0.0" }, "servers": [ { "url": "http://petstore.swagger.io/v1" } ], "security": [{"pass1": []}], "paths": { "/pets/{petId}": { "get": { "operationId": "showPetById", "tags": [ "pets" ], "summary": "Info for a specific pet", "parameters": [ { "description": "The id of the pet to retrieve", "in": "path", "name": "petId", "required": true, "schema": { "type": "string" } }, { "description": "Indicates if the age is wanted in the response object", "in": "query", "name": "wantAge", "schema": { "type": "boolean" } } ], "responses": { "200": { "description": "Expected response to a valid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } } } } } } }, "/pets": { "get": { "operationId": "listPets", "summary": "List all pets", "tags": [ "pets" ], "parameters": [ { "description": "How many items to return at one time (max 100)", "in": "query", "name": "limit", "required": false, "schema": { "type": "integer", "format": "int32" } } ], "responses": { "200": { "description": "An paged array of pets", "headers": { "x-next": { "schema": { "type": "string" }, "description": "A link to the next page of responses" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pets" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/Pets" } } } } } } } }, "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "basic" } }, "schemas": { "Pets": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } }, "Pet": { "required": [ "id", "name" ], "properties": { "tag": { "type": "string" }, "id": { "type": "integer", "format": "int64" }, "name": { "type": "string" }, "age": { "type": "integer" } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/plugin-cors.t0000644000175100001660000001476314766301334020700 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; our $cors_callback = 'main::cors_exchange'; use Mojolicious::Lite; get '/user' => sub { my $c = shift->openapi->cors_exchange($cors_callback)->openapi->valid_input or return; $c->render(json => {cors => 'cors_exchange', origin => $c->stash('origin')}); }, 'getUser'; put '/user' => sub { my $c = shift->openapi->cors_exchange->openapi->valid_input or return; $c->render(json => {created => time}); }, 'addUser'; put '/headers' => sub { my $c = shift->openapi->valid_input or return; $c->res->headers->access_control_allow_origin($c->req->headers->origin) if $c->req->headers->origin; $c->render(json => {h => 42}); }, 'headerValidation'; plugin OpenAPI => {url => 'data://main/cors.json', add_preflighted_routes => 1}; my $t = Test::Mojo->new; note 'Simple'; $t->get_ok('/api/user', {'Content-Type' => 'text/plain', Origin => 'http://bar.example'}) ->status_is(400)->json_is('/errors/0/message', 'Invalid Origin header.'); $t->get_ok('/api/user', {'Content-Type' => 'text/plain', Origin => 'http://foo.example'}) ->status_is(200)->header_is('Access-Control-Allow-Origin' => 'http://foo.example') ->json_is('/cors', 'cors_exchange')->json_is('/origin', 'http://foo.example'); $t->get_ok('/api/user', {Origin => 'http://foo.example'})->status_is(200) ->header_is('Access-Control-Allow-Origin' => 'http://foo.example'); note 'Preflighted'; $t->options_ok('/api/user', {'Content-Type' => 'text/plain', Origin => 'http://bar.example'}) ->status_is(400)->json_is('/errors/0/message', 'Invalid Origin header.'); $t->options_ok('/api/user', {'Content-Type' => 'text/plain', Origin => 'http://foo.example'}) ->status_is(200)->header_is('Access-Control-Allow-Origin' => 'http://foo.example') ->header_is('Access-Control-Allow-Headers' => 'X-Whatever, X-Something') ->header_is('Access-Control-Allow-Methods' => 'POST, GET, OPTIONS') ->header_is('Access-Control-Max-Age' => 86400)->content_is(''); $t->options_ok( '/api/user', { 'Access-Control-Request-Headers' => 'X-Foo, X-Bar', 'Access-Control-Request-Method' => 'GET', 'Content-Type' => 'text/plain', 'Origin' => 'http://foo.example' } )->status_is(200)->header_is('Access-Control-Allow-Origin' => 'http://foo.example') ->header_is('Access-Control-Allow-Headers' => 'X-Foo, X-Bar') ->header_is('Access-Control-Allow-Methods' => 'GET, PUT') ->header_is('Access-Control-Max-Age' => 1800)->content_is(''); note 'Default cors exchange'; $cors_callback = undef; $t->app->defaults(openapi_cors_allowed_origins => [qr{bar\.example}]); $t->app->defaults(openapi_cors_default_max_age => 42); $t->options_ok('/api/user', {'Origin' => 'http://bar.example', 'Access-Control-Request-Method' => 'GET'})->status_is(200) ->header_is('Access-Control-Allow-Origin' => 'http://bar.example') ->header_is('Access-Control-Max-Age' => 42)->content_is(''); note 'Actual request'; $t->options_ok('/api/user')->status_is(400) ->json_is('/errors/0/message', 'OPTIONS is only for preflighted CORS requests.'); $t->put_ok('/api/user', {'Origin' => 'http://bar.example'})->status_is(200) ->header_is('Access-Control-Allow-Origin' => 'http://bar.example')->json_has('/created'); $t->get_ok('/api/user')->status_is(200)->header_is('Access-Control-Allow-Origin' => undef) ->json_is('/origin', undef); $t->put_ok('/api/user')->status_is(200)->header_is('Access-Control-Allow-Origin' => undef) ->json_has('/created'); $t->put_ok('/api/headers')->status_is(200)->header_is('Access-Control-Allow-Origin' => undef) ->json_is('/h' => 42); note 'Using the spec'; $t->options_ok('/api/headers')->status_is(400)->json_is('/errors/0/path' => '/Origin'); $t->put_ok('/api/headers', {'Origin' => 'https://foo.example'})->status_is(400) ->json_is('/errors/0/path' => '/Origin'); $t->options_ok('/api/headers', {'Origin' => 'http://foo.example'})->status_is(400) ->json_is('/errors/0/path' => '/Origin'); $t->options_ok('/api/headers', {'Origin' => 'http://bar.example'})->status_is(200) ->header_is('Access-Control-Allow-Origin' => 'http://bar.example') ->header_is('Access-Control-Max-Age' => 42)->content_is(''); $t->put_ok('/api/headers', {'Origin' => 'https://bar.example'})->status_is(200) ->header_is('Access-Control-Allow-Origin' => 'https://bar.example')->json_is('/h' => 42); done_testing; sub cors_exchange { my $c = shift; my $req_h = $c->req->headers; my $headers = $req_h->header('Access-Control-Request-Headers'); my $method = $req_h->header('Access-Control-Request-Methods'); my $origin = $req_h->header('Origin'); return '/Origin' unless $origin eq 'http://foo.example'; return '/X-No-Can-Do' if $headers and $headers =~ /X-No-Can-Do/; return '/Access-Control-Request-Method' if $method and $method eq 'DELETE'; $c->stash(origin => $origin); # Set required Preflighted response header $c->res->headers->header('Access-Control-Allow-Origin' => $origin); # Set Preflighted response headers, instead of using the default $c->res->headers->header('Access-Control-Allow-Headers' => 'X-Whatever, X-Something') unless $c->req->headers->header('Access-Control-Request-Headers'); $c->res->headers->header('Access-Control-Allow-Methods' => 'POST, GET, OPTIONS') unless $c->req->headers->header('Access-Control-Request-Method'); $c->res->headers->header('Access-Control-Max-Age' => 86400) unless $c->req->headers->header('Access-Control-Request-Method'); return undef; } __DATA__ @@ cors.json { "swagger": "2.0", "info": { "version": "0.8", "title": "Test cors response" }, "basePath": "/api", "paths": { "/user": { "get": { "operationId": "getUser", "responses": { "200": { "description": "Get user", "schema": { "type": "object" } } } }, "put": { "operationId": "addUser", "responses": { "200": { "description": "Create user", "schema": { "type": "object" } } } } }, "/headers": { "parameters": [ { "in": "header", "name": "Origin", "type": "string", "pattern": "https?://bar.example" } ], "options": { "x-mojo-to": "#openapi_plugin_cors_exchange", "responses": { "200": { "description": "Cors exchange", "schema": { "type": "object" } } } }, "put": { "operationId": "headerValidation", "responses": { "200": { "description": "Cors put", "schema": { "type": "object" } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/v3-style-object.t0000644000175100001660000002353614766301334021366 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; get '/pets' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'getPets'; get '/petsBySimpleId/:id' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'getPetsBySimpleId'; get '/petsByExplodedSimpleId/:id' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'getPetsByExplodedSimpleId'; get '/petsByMatrixId#id' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'getPetsByMatrixId'; get '/petsByExplodedMatrixId#id' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'getPetsByExplodedMatrixId'; get '/petsByLabelId#id' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'getPetsByLabelId'; get '/petsByExplodedLabelId#id' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'getPetsByExplodedLabelId'; plugin OpenAPI => {url => 'data:///parameters.json'}; my $t = Test::Mojo->new; subtest 'style: deepObject' => sub { $t->get_ok('/api/pets')->status_is(200)->json_is('/do', undef); $t->get_ok('/api/pets?do[name]=birdy&do[birth-date][gte]=1970-01-01&do[numbers][0]=5') ->status_is(200) ->json_is('/do', {name => 'birdy', 'birth-date' => {gte => '1970-01-01'}, numbers => [5]}); $t->get_ok('/api/pets?do[numbers][0]=5&do[numbers][1]=10')->status_is(200) ->json_is('/do', {numbers => [5, 10]}); $t->get_ok('/api/pets?do[numbers][]=5&do[numbers][]=10')->status_is(200) ->json_is('/do', {numbers => [5, 10]}); $t->get_ok('/api/pets?do[numbers]=5&do[numbers]=10')->status_is(200) ->json_is('/do', {numbers => [5, 10]}); }; subtest 'style: form, explode: false' => sub { $t->get_ok('/api/pets')->status_is(200)->json_is('/ff', undef); $t->get_ok('/api/pets?ff=')->status_is(200)->json_is('/ff', {}); $t->get_ok('/api/pets?ff=name,birdy,age,3')->status_is(200) ->json_is('/ff', {name => 'birdy', age => 3}); }; subtest 'style: form, explode: true' => sub { $t->get_ok('/api/pets')->status_is(200)->json_is('/ft', {}); $t->get_ok('/api/pets?name=birdy&age=3')->status_is(200) ->json_is('/ft', {name => ['birdy'], age => 3}); $t->get_ok('/api/pets?name=birdy&age=3&name=birdy2')->status_is(200) ->json_is('/ft', {name => ['birdy', 'birdy2'], age => 3}); }; subtest 'style: spaceDelimited' => sub { $t->get_ok('/api/pets')->status_is(200)->json_is('/sf', undef); $t->get_ok('/api/pets?sf=')->status_is(200)->json_is('/sf', {}); $t->get_ok('/api/pets?sf=name%20birdy%20age%203')->status_is(200) ->json_is('/sf', {name => 'birdy', age => 3}); }; subtest 'style: pipeDelimited' => sub { $t->get_ok('/api/pets')->status_is(200)->json_is('/pf', undef); $t->get_ok('/api/pets?pf=')->status_is(200)->json_is('/pf', {}); $t->get_ok('/api/pets?pf=name|birdy|age|3')->status_is(200) ->json_is('/pf', {name => 'birdy', age => 3}); }; subtest 'style: simple, explode: false' => sub { $t->get_ok('/api/petsBySimpleId/category,bird,name,birdy')->status_is(200) ->json_is('/id', {category => 'bird', name => 'birdy'}); }; subtest 'style: simple, explode: true' => sub { $t->get_ok('/api/petsByExplodedSimpleId/category=bird,name=birdy')->status_is(200) ->json_is('/id', {category => 'bird', name => 'birdy'}); }; subtest 'style: matrix, explode: false' => sub { $t->get_ok('/api/petsByMatrixId;id=category,bird,name,birdy')->status_is(200) ->json_is('/id', {category => 'bird', name => 'birdy'}); }; subtest 'style: matrix, explode: true' => sub { $t->get_ok('/api/petsByExplodedMatrixId;category=bird;name=birdy')->status_is(200) ->json_is('/id', {category => 'bird', name => 'birdy'}); }; subtest 'style: label, explode: false' => sub { $t->get_ok('/api/petsByLabelId.category.bird.name.birdy')->status_is(200) ->json_is('/id', {category => 'bird', name => 'birdy'}); }; subtest 'style: label, explode: true' => sub { $t->get_ok('/api/petsByExplodedLabelId.category=bird.name=birdy')->status_is(200) ->json_is('/id', {category => 'bird', name => 'birdy'}); }; done_testing; __DATA__ @@ parameters.json { "openapi": "3.0.0", "info": { "license": { "name": "MIT" }, "title": "Swagger Petstore", "version": "1.0.0" }, "servers": [ { "url": "/api" } ], "paths": { "/pets": { "get": { "operationId": "getPets", "parameters": [ { "name": "do", "in": "query", "style": "deepObject", "explode": true, "schema": { "type": "object" } }, { "name": "ff", "in": "query", "style": "form", "explode": false, "schema": { "type": "object" } }, { "name": "ft", "in": "query", "style": "form", "explode": true, "schema": { "type": "object", "properties": { "name": {"type": "array", "items": {"type": "string"}} } } }, { "name": "sf", "in": "query", "style": "spaceDelimited", "explode": false, "schema": { "type": "object" } }, { "name": "pf", "in": "query", "style": "pipeDelimited", "explode": false, "schema": { "type": "object" } } ], "responses": { "200": { "description": "pet response", "content": { "*/*": { "schema": { "type": "object" } } } } } } }, "/petsBySimpleId/{id}": { "get": { "operationId": "getPetsBySimpleId", "parameters": [ { "name": "id", "in": "path", "required": true, "style": "simple", "explode": false, "schema": { "type": "object" } } ], "responses": { "200": { "description": "pet response", "content": { "*/*": { "schema": { "type": "object" } } } } } } }, "/petsByExplodedSimpleId/{id}": { "get": { "operationId": "getPetsByExplodedSimpleId", "parameters": [ { "name": "id", "in": "path", "required": true, "style": "simple", "explode": true, "schema": { "type": "object" } } ], "responses": { "200": { "description": "pet response", "content": { "*/*": { "schema": { "type": "object" } } } } } } }, "/petsByMatrixId/{id}": { "get": { "operationId": "getPetsByMatrixId", "parameters": [ { "name": "id", "in": "path", "required": true, "style": "matrix", "explode": false, "schema": { "type": "object" } } ], "responses": { "200": { "description": "pet response", "content": { "*/*": { "schema": { "type": "object" } } } } } } }, "/petsByExplodedMatrixId/{id}": { "get": { "operationId": "getPetsByExplodedMatrixId", "parameters": [ { "name": "id", "in": "path", "required": true, "style": "matrix", "explode": true, "schema": { "type": "object" } } ], "responses": { "200": { "description": "pet response", "content": { "*/*": { "schema": { "type": "object" } } } } } } }, "/petsByLabelId/{id}": { "get": { "operationId": "getPetsByLabelId", "parameters": [ { "name": "id", "in": "path", "required": true, "style": "label", "explode": false, "schema": { "type": "object" } } ], "responses": { "200": { "description": "pet response", "content": { "*/*": { "schema": { "type": "object" } } } } } } }, "/petsByExplodedLabelId/{id}": { "get": { "operationId": "getPetsByExplodedLabelId", "parameters": [ { "name": "id", "in": "path", "required": true, "style": "label", "explode": true, "schema": { "type": "object" } } ], "responses": { "200": { "description": "pet response", "content": { "*/*": { "schema": { "type": "object" } } } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/basic-bundle.t0000644000175100001660000000132614766301334020755 0ustar runnerdockeruse Mojo::Base -strict; use Mojo::File 'path'; use Test::More; use JSON::Validator::Schema::OpenAPIv2; # This test mimics what Mojolicious::Plugin::OpenAPI does when loading # a spec from a file that Mojolicious locates with a '..' # It checks that a $ref to something that's under /responses doesn't # get picked as remote, or if so that it doesn't make an invalid spec! my $validator = JSON::Validator::Schema::OpenAPIv2->new; my $bundlecheck_path = path(path(__FILE__)->dirname, 'spec', File::Spec->updir, 'spec', 'bundlecheck.json'); my $bundled = $validator->data($bundlecheck_path)->bundle->data; eval { JSON::Validator->new->load_and_validate_schema($bundled) }; is $@, '', 'bundled schema is valid'; done_testing; Mojolicious-Plugin-OpenAPI-5.11/t/basic-coerce.t0000644000175100001660000000234414766301334020745 0ustar runnerdockeruse Mojo::Base -strict; use Mojolicious; use Test::Mojo; use Test::More; my $coerced = t(); $coerced->post_ok('/api/user')->status_is(200)->json_is('/age', 34); $coerced->post_ok('/api/user', json => [{}])->status_is(400) ->json_is('/errors/0/message', 'Expected object - got array.'); my $strict = t(coerce => {}); $strict->post_ok('/api/user')->status_is(500)->json_has('/errors'); sub t { my $t = Test::Mojo->new(Mojolicious->new); $t->app->routes->post( '/user' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {age => '34'}); # '34' is not an integer } )->name('user'); $t->app->plugin(OpenAPI => {url => 'data://main/user.json', @_}); $t; } done_testing; __DATA__ @@ user.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Pets" }, "schemes" : [ "http" ], "basePath" : "/api", "paths" : { "/user" : { "post" : { "x-mojo-name" : "user", "parameters": [ {"in": "body", "name": "body", "schema": {"type": "object"}} ], "responses" : { "200": { "description": "User", "schema": { "properties": { "age": { "type": "integer"} } } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/plugin-spec-renderer-v3.t0000644000175100001660000000074014766301334023004 0ustar runnerdockeruse Mojo::Base -strict; use Mojo::File 'path'; use Test::Mojo; use Test::More; use Mojolicious::Lite; plugin OpenAPI => {url => path(__FILE__)->dirname->child(qw(spec v3-petstore.json))}; my $t = Test::Mojo->new; $t->get_ok('/v1')->status_is(200)->json_like('/servers/0/url', qr{:\d+/v1$}); $t->get_ok('/v1.json')->status_is(200)->json_like('/servers/0/url', qr{:\d+/v1$}); $t->get_ok('/v1.html')->status_is(200)->element_exists('ul.unstyled li a[href$="/v1"]'); done_testing; Mojolicious-Plugin-OpenAPI-5.11/t/jv-recursion.t0000644000175100001660000000374714766301334021064 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use JSON::Validator::Schema::OpenAPIv2; TODO: { todo_skip 'At this moment in spacetime, I do not know how to suppport both a recursive schema and a recusrive data structure', 2; my ($data, @errors) = ({}); $data->{rec} = $data; eval { local $SIG{ALRM} = sub { die 'Recursion!' }; alarm 2; @errors = JSON::Validator::Schema::Draft4->new('data://main/spec.json')->validate({top => $data}); }; is $@, '', 'no error'; is_deeply(\@errors, [], 'avoided recursion'); } note 'This part of the test checks that we don\'t go into an infite loop'; eval { my $validator = JSON::Validator::Schema::OpenAPIv2->new; $validator->data('data://main/user.json')->errors; $validator->data($validator->data)->errors; }; ok !$@, 'handle $schema with recursion'; done_testing; __DATA__ @@ spec.json { "properties": { "top": { "$ref": "#/definitions/again" } }, "definitions": { "again": { "anyOf": [ {"type": "string"}, { "type": "object", "properties": { "rec": {"$ref": "#/definitions/again"} } } ] } } } @@ user.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "User schema" }, "schemes" : [ "http" ], "basePath" : "/api", "paths" : { "/user" : { "post" : { "operationId" : "User", "parameters": [{ "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/user" } }], "responses" : { "200": { "description": "response", "schema": { "type": "object" } } } } } }, "definitions": { "user": { "type": "object", "properties": { "name": { "type": "string" }, "siblings": { "type": "array", "items": { "$ref": "#/definitions/user" } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/v3-style-array.t0000644000175100001660000002060714766301334021232 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; get '/pets' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'getPets'; get '/pets/:id' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'getPetsById'; get '/petsByLabelId#id' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'getPetsByLabelId'; get '/petsByExplodedLabelId#id' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'getPetsByExplodedLabelId'; get '/petsByMatrixId#id' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'getPetsByMatrixId'; get '/petsByExplodedMatrixId#id' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'getPetsByExplodedMatrixId'; plugin OpenAPI => {url => 'data:///parameters.json'}; my $t = Test::Mojo->new; subtest 'Expected array - got null' => sub { $t->get_ok('/api/pets')->status_is(400)->json_is('/errors/0/path', '/ri'); }; subtest 'Expected integer - got number.' => sub { $t->get_ok('/api/pets?ri=1.3')->status_is(400)->json_is('/errors/0/path', '/ri/0'); }; subtest 'Not enough items: 1\/2' => sub { $t->get_ok('/api/pets?ri=3&ml=5')->status_is(400)->json_is('/errors/0/path', '/ml'); }; subtest 'Valid, in path' => sub { $t->get_ok('/api/pets/10,11,12')->status_is(200)->json_is('/id', [qw(10 11 12)]); $t->get_ok('/api/pets/10')->status_is(200)->content_like(qr{"id":\[10\]}); $t->get_ok('/api/petsByLabelId.3,4,5')->status_is(200)->json_is('/id', [qw(3 4 5)]); $t->get_ok('/api/petsByLabelId.5')->status_is(200)->json_is('/id', [5]); $t->get_ok('/api/petsByExplodedLabelId.3.4.5')->status_is(200)->json_is('/id', [qw(3 4 5)]); $t->get_ok('/api/petsByExplodedLabelId.5')->status_is(200)->json_is('/id', [5]); $t->get_ok('/api/petsByMatrixId;id=3,4,5')->status_is(200)->json_is('/id', [qw(3 4 5)]); $t->get_ok('/api/petsByMatrixId;id=5')->status_is(200)->json_is('/id', [5]); $t->get_ok('/api/petsByExplodedMatrixId;id=3;id=4;id=5')->status_is(200) ->json_is('/id', [qw(3 4 5)]); $t->get_ok('/api/petsByExplodedMatrixId;id=5')->status_is(200)->json_is('/id', [5]); }; subtest 'Valid, in query' => sub { $t->get_ok('/api/pets?ri=3&ml=4&ml=2&no=5')->status_is(200)->json_is('/ri', [3]) ->content_like(qr{"ml":\["4","2"\]})->content_like(qr{"no":\[5\]}); $t->get_ok('/api/pets?ri=3&no=5,6&sp=7 8 9&pi=10|11')->status_is(200)->json_is('/no', [5, 6]) ->json_is('/sp', [7, 8, 9])->json_is('/pi', [10, 11]); }; done_testing; __DATA__ @@ parameters.json { "openapi": "3.0.0", "info": { "license": { "name": "MIT" }, "title": "Swagger Petstore", "version": "1.0.0" }, "servers": [ { "url": "/api" } ], "paths": { "/pets/{id}": { "get": { "operationId": "getPetsById", "parameters": [ { "name": "id", "in": "path", "required": true, "style": "simple", "schema": { "type": "array", "items": { "type": "integer" }, "minItems": 1 } } ], "responses": { "200": { "description": "pet response", "content": { "*/*": { "schema": { "type": "object" } } } } } } }, "/petsByLabelId{id}": { "get": { "operationId": "getPetsByLabelId", "parameters": [ { "name": "id", "in": "path", "required": true, "style": "label", "explode": false, "schema": { "type": "array", "items": { "type": "integer" }, "minItems": 1 } } ], "responses": { "200": { "description": "pet response", "content": { "*/*": { "schema": { "type": "object" } } } } } } }, "/petsByExplodedLabelId{id}": { "get": { "operationId": "getPetsByExplodedLabelId", "parameters": [ { "name": "id", "in": "path", "required": true, "style": "label", "explode": true, "schema": { "type": "array", "items": { "type": "integer" }, "minItems": 1 } } ], "responses": { "200": { "description": "pet response", "content": { "*/*": { "schema": { "type": "object" } } } } } } }, "/petsByMatrixId{id}": { "get": { "operationId": "getPetsByMatrixId", "parameters": [ { "name": "id", "in": "path", "required": true, "style": "matrix", "explode": false, "schema": { "type": "array", "items": { "type": "integer" }, "minItems": 1 } } ], "responses": { "200": { "description": "pet response", "content": { "*/*": { "schema": { "type": "object" } } } } } } }, "/petsByExplodedMatrixId{id}": { "get": { "operationId": "getPetsByExplodedMatrixId", "parameters": [ { "name": "id", "in": "path", "required": true, "style": "matrix", "explode": true, "schema": { "type": "array", "items": { "type": "integer" }, "minItems": 1 } } ], "responses": { "200": { "description": "pet response", "content": { "*/*": { "schema": { "type": "object" } } } } } } }, "/pets": { "get": { "operationId": "getPets", "parameters": [ { "name": "no", "in": "query", "style": "form", "explode": false, "schema": { "type": "array", "items": { "type": "integer" }, "minItems": 1 } }, { "name": "ml", "in": "query", "style": "form", "explode": true, "schema": { "type": "array", "items": { "type": "string" }, "minItems": 2 } }, { "name": "ri", "in": "query", "required": true, "style": "form", "explode": true, "schema": { "type": "array", "items": { "type": "integer" }, "minItems": 1 } }, { "name": "sp", "in": "query", "style": "spaceDelimited", "schema": { "type": "array", "items": { "type": "integer" } } }, { "name": "pi", "in": "query", "style": "pipeDelimited", "schema": { "type": "array", "items": { "type": "integer" } } } ], "responses": { "200": { "description": "pet response", "content": { "*/*": { "schema": { "type": "object" } } } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/v2-defaults.t0000644000175100001660000001276114766301334020566 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; package Test::Controller::Echo; use Mojo::Base 'Mojolicious::Controller'; sub any { my $c = shift->openapi->valid_input or return; my $name = $c->stash('name') ? {param => $c->param('name'), stash => $c->stash('name')} : {controller => $c->param('name'), form => $c->req->body_params->param('name')}; $c->render( openapi => { days => {controller => $c->param('days'), url => $c->req->query_params->param('days')}, format => $c->stash('format'), name => $name, route => { bar => $c->stash('bar'), constraints => $c->match->endpoint->pattern->constraints, foo => $c->stash('foo'), namespace => $c->stash('namespace') }, x_foo => {header => $c->req->headers->header('X-Foo')}, validation => $c->validation->output, } ); } package main; use Mojolicious::Lite; get '/echo' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {bool => $c->param('bool')}); }, 'echo'; get '/echo/:whatever' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {this_stack => $c->match->stack->[-1], whatever => $c->param('whatever')}); }, 'whatever'; get '/param-has-ref' => sub { my $c = shift->openapi->valid_input or return; my $params = $c->validation->output; $c->render(status => 200, openapi => $params->{pcversion}); }, 'ParamsHasRef'; plugin OpenAPI => {url => 'data://main/def.json'}; my $t = Test::Mojo->new; $t->get_ok('/api/echo?bool=false')->status_is(200)->json_is('/bool' => Mojo::JSON->false); $t->get_ok('/api/echo?bool=true')->status_is(200)->json_is('/bool' => Mojo::JSON->true); $t->get_ok('/api/echo')->status_is(200)->json_is('/bool' => Mojo::JSON->true); $t->get_ok('/api/echo/something')->status_is(200)->json_is('/this_stack/whatever' => 'something') ->json_is('/whatever' => 'something'); $t->get_ok('/api/param-has-ref?x=42')->status_is(200)->content_is('"10.1.0"'); $t->post_ok('/api/echo-controller.js')->status_is(200) ->json_is('/days' => {controller => 42, url => 42})->json_is('/format', 'js') ->json_is('/name', {controller => 'batman', form => 'batman'})->json_is( '/route', { bar => 42, constraints => {format => [qw(js txt)]}, foo => [42], namespace => 'Test::Controller' } )->json_is('/x_foo', {header => 'yikes'}) ->json_is('/validation', {days => 42, name => 'batman', 'X-Foo' => 'yikes', enumParam => '10.1.0'}); $t->get_ok('/api/echo-controller/batman')->status_is(200) ->json_is('/days' => {controller => 42, url => 42}) ->json_is('/name', {param => 'batman', stash => 'batman'}); ok !$t->tx->res->json->{x_foo}{header}, 'x_foo header is not set'; done_testing; __DATA__ @@ def.json { "swagger": "2.0", "info": { "version": "0.8", "title": "Pets" }, "schemes": [ "http" ], "basePath": "/api", "parameters": { "PCVersion": { "name": "pcversion", "in": "query", "type": "string", "enum": [ "9.6.1", "10.1.0" ], "default": "10.1.0", "description": "version of commands which will run on backend" } }, "paths": { "/echo/{whatever}": { "get": { "x-mojo-name": "whatever", "parameters": [ { "in": "path", "name": "whatever", "type": "string", "required": true } ], "responses": { "200": { "description": "Echo response", "schema": { "type": "object" } } } } }, "/echo": { "get": { "x-mojo-name": "echo", "parameters": [ { "in": "query", "name": "bool", "type": "boolean", "default": true } ], "responses": { "200": { "description": "Echo response", "schema": { "type": "object" } } } } }, "/echo-controller": { "post": { "x-mojo-to": ["echo#any", {"foo": [42]}, "bar", "42", ["format", ["js", "txt"]], "namespace", "Test::Controller"], "parameters": [ { "in": "query", "name": "days", "type": "number", "default": 42 }, { "in": "formData", "name": "name", "type": "string", "default": "batman" }, { "in": "query", "name": "enumParam", "type": "string", "default": "10.1.0", "enum": [ "9.6.1", "10.1.0" ] }, { "in": "header", "name": "X-Foo", "type": "string", "default": "yikes" } ], "responses": { "200": { "description": "Echo response", "schema": { "type": "object" } } } } }, "/echo-controller/{name}": { "get": { "x-mojo-to": ["namespace", "Test::Controller", "controller", "echo", "action", "any"], "parameters": [ { "in": "path", "name": "name", "type": "string", "required": true }, { "in": "query", "name": "days", "type": "number", "default": 42 } ], "responses": { "200": { "description": "Echo response", "schema": { "type": "object" } } } } }, "/param-has-ref": { "get": { "operationId": "ParamsHasRef", "parameters": [ { "$ref": "#/parameters/PCVersion" }, { "name": "x", "in": "query", "type": "string", "description": "x" } ], "responses": { "200": { "description": "thing", "schema": { "type": "string" } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/plugin-spec-renderer-standalone.t0000644000175100001660000000372514766301334024612 0ustar runnerdockeruse Mojo::Base -strict; use Mojo::File 'path'; use Mojolicious; use Test::Mojo; use Test::More; sub VERSION {1.42} my $petstore = path(__FILE__)->dirname->child(qw(spec v2-petstore.json)); my $app = Mojolicious->new; # All options are ignored when loaded as standalone plugin $app->plugin('Mojolicious::Plugin::OpenAPI::SpecRenderer' => {url => $petstore->to_string, spec_route_name => 'my.cool.api', version_from_class => 'main'}); my $custom_spec = JSON::Validator->new->schema($petstore->to_string)->schema->bundle; $app->routes->get('/my-unknown-doc' => sub { shift->openapi->render_spec }); $app->routes->get( '/my-cool-doc' => [format => [qw(html json)]], {format => undef}, sub { $_[0]->openapi->render_spec($_[0]->param('path'), $custom_spec) } ); my $t = Test::Mojo->new($app); $t->get_ok('/my-cool-doc.json')->status_is(200)->json_is('/basePath', '/v1') ->json_is('/host', 'petstore.swagger.io')->json_is('/info/version', '1.0.0'); $t->get_ok('/my-cool-doc.json?path=/pets/{petId}')->status_is(200) ->json_is('/$schema', 'http://json-schema.org/draft-04/schema#') ->json_is('/title', 'Swagger Petstore')->json_is('/description', '') ->json_is('/get/operationId', 'showPetById') ->json_is('/get/responses/200/schema/$ref', '#/definitions/Pets') ->json_is('/definitions/Pets/type', 'array'); $t->get_ok('/my-cool-doc.json?method=get&path=/pets/{petId}')->status_is(200) ->json_is('/$schema', 'http://json-schema.org/draft-04/schema#') ->json_is('/title', 'Swagger Petstore')->json_is('/operationId', 'showPetById'); $t->get_ok('/my-unknown-doc')->status_is(500) ->json_is('/errors/0/message', 'No specification to render.'); $t->get_ok('/my-cool-doc.html')->status_is(200)->text_is('h3#op-post--pets a', 'createPets'); SKIP: { skip 'Text::Markdown is not installed', 2 unless eval 'require Text::Markdown;1'; $t->text_is('div.spec-description p', 'Null response'); } done_testing; Mojolicious-Plugin-OpenAPI-5.11/t/v3-tutorial.t0000644000175100001660000000511714766301334020620 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; make_app(); make_controller(); my $t = Test::Mojo->new('Myapp'); $t->get_ok('/api')->status_is(200)->json_is('/info/title', 'Some awesome API'); $t->get_ok('/api/pets' => {'Content-Type' => 'application/json'})->status_is(200) ->json_is('/pets/0/name', 'kit-e-cat'); done_testing; sub make_app { eval <<'HERE' or die $@; package Myapp; use Mojo::Base "Mojolicious"; sub startup { my $app = shift; $app->plugin("OpenAPI" => {url => "data://main/myapi.json"}); } $ENV{"Myapp.pm"} = 1; HERE } sub make_controller { eval <<'HERE' or die $@; package Myapp::Controller::Pet; use Mojo::Base "Mojolicious::Controller"; sub list { # Do not continue on invalid input and render a default 400 # error document. my $c = shift; $c = $c->openapi->valid_input or return; # $c->openapi->valid_input copies valid data to validation object, # and the normal Mojolicious api works as well. my $input = $c->validation->output; my $age = $c->param("age"); # same as $input->{age} my $body = $c->req->json; # same as $input->{body} # $output will be validated by the OpenAPI spec before rendered my $output = {pets => [{name => "kit-e-cat"}]}; $c->render(openapi => $output); } $ENV{"Myapp/Controller/Pet.pm"} = 1; HERE } __DATA__ @@ myapi.json { "openapi": "3.0.2", "info": { "version": "1.0", "title": "Some awesome API" }, "paths": { "/pets": { "get": { "operationId": "getPets", "x-mojo-name": "get_pets", "x-mojo-to": "pet#list", "summary": "Finds pets in the system", "parameters": [ { "in": "query", "name": "age", "schema": { "type": "integer" } } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object" } } } }, "responses": { "200": { "description": "Pet response", "content": { "application/json": { "schema": { "type": "object", "properties": { "pets": { "type": "array", "items": { "type": "object" } } } } } } } } } } }, "servers": [ { "url": "/api" } ] } Mojolicious-Plugin-OpenAPI-5.11/t/v3-file.t0000644000175100001660000000317214766301334017673 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; post '/upload' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {size => $c->req->upload('image')->size}); }, 'upload'; plugin OpenAPI => {url => 'data://main/openapi.yaml'}; my $t = Test::Mojo->new; $t->post_ok('/api/upload', form => {foo => 42})->status_is(400) ->json_is('/errors/0', {message => 'Missing property.', path => '/body/image'}); my $image = Mojo::Asset::Memory->new->add_chunk('smileyface'); $t->post_ok( '/api/upload', {Accept => 'application/json'}, form => {id => 1, image => {file => $image}} )->content_like(qr{"size"})->status_is(200); done_testing; __DATA__ @@ openapi.yaml --- openapi: 3.0.0 info: title: Upload test version: 1.0.0 servers: - url: http://example.com/api paths: /upload: post: operationId: upload requestBody: required: true content: application/x-www-form-urlencoded: schema: required: [ image ] properties: id: type: string image: type: string format: binary multipart/form-data: schema: required: [ image ] properties: image: type: string format: binary responses: 200: description: Accepted content: application/json: schema: required: [ size ] properties: size: type: integer Mojolicious-Plugin-OpenAPI-5.11/t/basic-custom-validation.t0000644000175100001660000000154014766301334023144 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; my $age = 43; get '/custom' => sub { my $c = shift; my @errors = $c->openapi->validate; return $c->render(text => sprintf '%s errors', int @errors) if @errors; return $c->render(text => 'cool beans'); }, 'get_custom'; plugin OpenAPI => {url => 'data://main/custom.json'}; my $t = Test::Mojo->new; $t->get_ok('/api/custom?i=42')->content_is('cool beans'); $t->get_ok('/api/custom?i=nok')->content_is('1 errors'); done_testing; __DATA__ @@ custom.json --- swagger: "2.0" info: version: "1.0" title: Custom validation basePath: /api paths: /custom: get: operationId: get_custom parameters: - name: i in: query type: integer responses: 200: description: ok schema: type: file Mojolicious-Plugin-OpenAPI-5.11/t/v3-basic.t0000644000175100001660000000704114766301334020034 0ustar runnerdockeruse Mojo::Base -strict; use Mojo::File 'path'; use Test::Mojo; use Test::More; use Mojolicious::Lite; get '/pets/:petId' => sub { my $c = shift->openapi->valid_input or return; my $input = $c->validation->output; my $output = {id => $input->{petId}, name => 'Cow'}; $output->{age} = 6 if $input->{wantAge}; $c->render(openapi => $output); }, 'showPetById'; get '/pets' => sub { my $c = shift->openapi->valid_input or return; $c->res->headers->header('x-next' => $c->param('limit') // 0); $c->render(openapi => $c->param('limit') ? [] : {}); }, 'listPets'; post '/pets' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => '', status => 201); }, 'createPets'; my $openapi_plugin = plugin OpenAPI => { url => path(__FILE__)->dirname->child(qw(spec v3-petstore.json)), renderer => sub { my ($c, $data) = @_; my $ct = $c->stash('openapi_negotiated_content_type') || 'application/json'; return '' if $c->stash('status') == 201; $c->res->headers->content_type($ct); return '' if $ct =~ m!^application/xml!; return Mojo::JSON::encode_json($data); } }; my $t = Test::Mojo->new; $t->get_ok('/v1.json')->status_is(200)->json_like('/servers/0/url', qr{^http://[^/]+/v1$}) ->json_hasnt('/basePath'); ok !$openapi_plugin->validator->data->{basePath}, 'basePath was not added'; $t->get_ok('/v1/pets?limit=invalid', {Accept => 'application/json'})->status_is(400) ->json_is('/errors/0/path', '/limit') ->json_is('/errors/0/message', 'Expected integer - got string.'); $t->get_ok('/v1/pets?limit=5', {Accept => 'application/json'})->status_is(200) ->header_is('x-next', 5)->content_is('[]'); $t->get_ok('/v1/pets?limit=10', {Accept => 'not/*'})->status_is(400) ->json_is('/errors/0/message', 'Expected application/json, application/xml - got not/*.'); $t->get_ok('/v1/pets?limit=0', {Accept => 'application/json'})->status_is(500) ->json_is('/errors/0/message', 'Expected array - got object.'); $t->get_ok('/v1/pets?limit=10', {Accept => 'application/json'})->status_is(200) ->header_like('Content-Type' => qr{^application/json})->content_is('[]'); $t->get_ok('/v1/pets?limit=10', {Accept => 'application/*'})->status_is(200) ->header_like('Content-Type' => qr{^application/json})->content_is('[]'); $t->get_ok('/v1/pets?limit=10', {Accept => 'text/html,application/xml;q=0.9,*/*;q=0.8'}) ->status_is(200)->header_like('Content-Type' => qr{^application/xml})->content_is(''); $t->get_ok('/v1/pets?limit=10', {Accept => 'text/html,*/*;q=0.8'})->status_is(200) ->header_like('Content-Type' => qr{^application/json})->content_is('[]'); $t->get_ok('/v1/pets?limit=10', {Accept => 'application/json'})->status_is(200)->content_is('[]'); $t->post_ok('/v1/pets', {Accept => 'application/json', Cookie => 'debug=foo'})->status_is(400) ->json_is('/errors/0/message', 'Missing property.')->json_is('/errors/0/path', '/body'); $t->post_ok('/v1/pets', {Cookie => 'debug=1'}, json => {id => 1, name => 'Supercow'}) ->status_is(201)->content_is(''); $t->post_ok('/v1/pets', form => {id => 1, name => 'Supercow'})->status_is(201)->content_is(''); $t->get_ok('/v1/pets/23?wantAge=yes', {Accept => 'application/json'})->status_is(400) ->json_is('/errors/0/message', 'Expected boolean - got string.'); $t->get_ok('/v1/pets/23?wantAge=true', {Accept => 'application/json'})->status_is(200) ->json_is('/id', 23)->json_is('/age', 6); $t->get_ok('/v1/pets/23?wantAge=false', {Accept => 'application/json'})->status_is(200) ->json_is('/id', 23)->json_is('/age', undef); done_testing; Mojolicious-Plugin-OpenAPI-5.11/t/basic-correct-order-of-paths.t0000644000175100001660000000234614766301334024000 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojo::Util 'encode'; use Mojolicious::Lite; post '/decode' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {decode => 1}); }, 'decode'; post '/:id' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {id => $c->param('id')}); }, 'id'; plugin OpenAPI => {url => "data://main/correct-order.json"}; my $t = Test::Mojo->new; $t->post_ok('/api/foo')->status_is(200)->json_is('/id', 'foo')->content_like(qr{id}); $t->post_ok('/api/decode')->status_is(200)->json_is('/decode', 1)->content_like(qr{decode}); done_testing; __DATA__ @@ correct-order.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "File" }, "schemes" : [ "http" ], "basePath" : "/api", "paths" : { "/decode": { "post": { "x-mojo-name": "decode", "responses": { "200": { "description": "Success" } } } }, "/{id}": { "post": { "x-mojo-name": "id", "parameters": [ { "name": "id", "in": "path", "required": true, "type": "string" } ], "responses": { "200": { "description": "Success" } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/data/0000755000175100001660000000000014766301352017147 5ustar runnerdockerMojolicious-Plugin-OpenAPI-5.11/t/data/image.jpeg0000644000175100001660000000002114766301334021071 0ustar runnerdockersome binary data Mojolicious-Plugin-OpenAPI-5.11/t/v2-discriminator.t0000644000175100001660000000630514766301334021623 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; post '/pets' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->req->json); }, 'addPet'; plugin OpenAPI => {url => 'data://main/discriminator.json'}; exit app->start(@ARGV) if @ARGV and $ARGV[0] eq 'routes'; my $t = Test::Mojo->new; my %cat = (name => 'kit-e-cat', petType => 'Cat', huntingSkill => "adventurous"); my %dog = (name => 'dog-e-dog', petType => 'Dog', packSize => 4); # jhthorsen: The error message is not very good. # I think this must be fixed in JSON::Validator. # {"errors":[{"message":"allOf failed: Missing property.","path":"\/body"}]} $t->post_ok('/api/pets' => json => {%cat, petType => 'Dog'})->status_is(400) ->json_like('/errors/0/message', qr{Missing property}); $t->post_ok('/api/pets' => json => {%cat})->status_is(200); $t->post_ok('/api/pets' => json => {%dog, petType => 'Cat'})->status_is(400) ->json_like('/errors/0/message', qr{Missing property}); $t->post_ok('/api/pets' => json => {%dog})->status_is(200); $t->post_ok('/api/pets' => json => {%dog, petType => ''})->status_is(400) ->json_is('/errors/0/message', 'Discriminator petType has no value.'); $t->post_ok('/api/pets' => json => {%dog, petType => 'Hamster'})->status_is(400) ->json_is('/errors/0/message', 'No definition for discriminator Hamster.'); done_testing; __DATA__ @@ discriminator.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Test discriminator" }, "consumes" : [ "application/json" ], "produces" : [ "application/json" ], "schemes" : [ "http" ], "basePath" : "/api", "paths" : { "/pets" : { "post" : { "operationId" : "addPet", "parameters" : [ { "in": "body", "name": "body", "schema": { "$ref" : "#/definitions/Pet" } } ], "responses" : { "200": { "description": "pet response", "schema": { "type": "object" } } } } } }, "definitions": { "Pet": { "type": "object", "discriminator": "petType", "required": [ "name", "petType" ], "properties": { "name": { "type": "string" }, "petType": { "type": "string" } } }, "Cat": { "description": "A representation of a cat", "allOf": [ { "$ref": "#/definitions/Pet" }, { "type": "object", "required": [ "huntingSkill" ], "properties": { "huntingSkill": { "type": "string", "description": "The measured skill for hunting", "default": "lazy", "enum": [ "clueless", "lazy", "adventurous", "aggressive" ] } } } ] }, "Dog": { "description": "A representation of a dog", "allOf": [ { "$ref": "#/definitions/Pet" }, { "type": "object", "required": [ "packSize" ], "properties": { "packSize": { "type": "integer", "format": "int32", "description": "the size of the pack the dog is from", "default": 0, "minimum": 0 } } } ] } } } Mojolicious-Plugin-OpenAPI-5.11/t/basic-autorender.t0000644000175100001660000000546414766301334021663 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; my %inline; { use Mojolicious::Lite; app->routes->namespaces(['MyApp::Controller']); get('/die' => sub { die 'Oh noes!' }, 'Die'); get('/inline' => sub { shift->render(%inline) }, 'Inline'); get '/not-found' => sub { shift->render(openapi => {this_is_fine => 1}, status => 404) }, 'NotFound'; plugin OpenAPI => {url => 'data://main/hook.json'}; } my $t = Test::Mojo->new; $t->app->mode('development'); # Exception $t->get_ok('/api/die')->status_is(500)->json_is('/errors/0/message', 'Internal Server Error.'); # Not implemented $t->get_ok('/api/todo')->status_is(404)->json_is('/errors/0/message', 'Not Found.'); # Implemented, but Not Found define_controller(); $t->get_ok('/api/todo')->status_is(404)->json_is('/errors/0/message', 'Not Found.'); $t->post_ok('/api/todo')->status_is(200)->json_is('/todo', 42); # Custom Not Found response $t->get_ok('/api/not-found')->status_is(404)->json_is('/this_is_fine', 1); # Custom Not Found template (mode) $t->get_ok('/THIS_IS_NOT_FOUND')->status_is(404)->content_like(qr{Not found development}); # Fallback to default renderer $inline{template} = 'inline'; $t->get_ok('/api/inline')->status_is(200); #->content_like(qr{Too cool}); $inline{openapi} = 'openapi is cool'; $t->get_ok('/api/inline')->status_is(200)->content_like(qr{openapi is cool}); done_testing; sub define_controller { eval <<'HERE' or die; package MyApp::Controller::Dummy; use Mojo::Base 'Mojolicious::Controller'; sub todo { my $c = shift->openapi->valid_input or return; $c->render(openapi => {todo => 42}); } 1; HERE } package main; __DATA__ @@ hook.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Test before_render hook" }, "basePath" : "/api", "paths" : { "/die" : { "get" : { "operationId" : "Die", "responses" : { "200": { "description": "response", "schema": { "type": "object" } } } } }, "/inline" : { "get" : { "operationId" : "Inline", "responses" : { "200": { "description": "response", "schema": { "type": "string" } } } } }, "/not-found" : { "get" : { "operationId" : "NotFound", "responses" : { "404": { "description": "response", "schema": { "type": "object" } } } } }, "/todo" : { "post" : { "x-mojo-to": "dummy#todo", "operationId" : "Auto", "parameters" : [ { "in": "body", "name": "body", "schema": { "type" : "object" } } ], "responses" : { "200": { "description": "response", "schema": { "type": "object" } } } } } } } @@ inline.html.ep Too cool @@ not_found.html.ep Not found @@ not_found.development.html.ep Not found development Mojolicious-Plugin-OpenAPI-5.11/t/v3-nullable.t0000644000175100001660000000420014766301334020543 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; my %data = (id => 42); get '/nullable-data' => \&action_null, 'withNullable'; get '/nullable-ref' => \&action_null, 'withNullableRef'; plugin OpenAPI => {url => 'data:///nullable.json'}; my $t = Test::Mojo->new; $t->get_ok('/v1/nullable-data')->status_is(500); $data{name} = undef; $t->get_ok('/v1/nullable-data')->status_is(200); $data{name} = 'batgirl'; $t->get_ok('/v1/nullable-data')->status_is(200); $t->get_ok('/v1/nullable-ref')->status_is(200); done_testing; sub action_null { my $c = shift->openapi->valid_input or return; $c->render(openapi => \%data); } __DATA__ @@ nullable.json { "openapi": "3.0.0", "info": { "license": { "name": "MIT" }, "title": "Swagger Petstore", "version": "1.0.0" }, "servers": [ { "url": "http://petstore.swagger.io/v1" } ], "paths": { "/nullable-data": { "get": { "operationId": "withNullable", "summary": "Dummy", "responses": { "200": { "description": "type:[null, string, ...] does the same", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WithNullable" } } } } } } }, "/nullable-ref": { "get": { "operationId": "withNullableRef", "summary": "Dummy", "responses": { "200": { "description": "type:[null, string, ...] does the same", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WithNullableRef" } } } } } } } }, "components": { "schemas": { "WithNullable": { "required": [ "id", "name" ], "properties": { "id": { "type": "integer", "format": "int64" }, "name": { "type": "string", "nullable": true } } }, "WithNullableRef": { "properties": { "name": { "$ref": "#/components/schemas/WithNullable/properties/name" } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/basic-invalid-json-input.t0000644000175100001660000000164314766301334023240 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; post '/invalid' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {x => 42}); }, 'invalid'; plugin OpenAPI => {url => 'data://main/spec.json', default_response => undef}; my $t = Test::Mojo->new; $t->post_ok('/api/invalid')->status_is(400)->json_is('/errors/0/message', 'Missing property.'); done_testing; __DATA__ @@ spec.json { "swagger" : "2.0", "info" : { "version": "0.1", "title" : "Test response codes" }, "basePath" : "/api", "paths" : { "/invalid": { "post" : { "operationId" : "invalid", "parameters": [ {"in": "body", "name": "body", "required": true, "schema": {"type": "object"}} ], "responses" : { "200": { "description": "Info", "schema": { "type": "object" } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/v2-swagger.t0000644000175100001660000000126014766301334020406 0ustar runnerdockeruse Mojo::Base -strict; use Mojo::File 'path'; use Test::Mojo; use Test::More; use Mojolicious::Lite; subtest 'check that we can load swagger.yaml' => sub { eval { plugin OpenAPI => {url => path(__FILE__)->dirname->child(qw(spec swagger swagger.yaml))}; ok 1, 'spec loaded'; } or do { diag $@; ok 0, 'spec loaded'; }; }; my $t = Test::Mojo->new; subtest 'check that we can add DefaultResponse to paths/ref.yaml' => sub { $t->get_ok('/swagger.json')->status_is(200) ->json_is('/paths/~1external~1ref/get/responses/200/description', 'Ref response') ->json_is('/paths/~1external~1ref/get/responses/500/description', 'Default response.'); }; done_testing; Mojolicious-Plugin-OpenAPI-5.11/t/v2-formats.t0000644000175100001660000000746514766301334020437 0ustar runnerdockeruse lib '.'; use JSON::Validator::Schema::OpenAPIv2; use JSON::Validator::Util qw(E); use Test::More; my $schema = {type => 'object', properties => {v => {type => 'string'}}}; my $validator = JSON::Validator::Schema::OpenAPIv2->new; sub validate_ok { my ($data, $schema, @expected) = @_; my $descr = @expected ? "errors: @expected" : "valid: " . Mojo::JSON::encode_json($data); my @errors = $validator->data($schema)->validate($data); is_deeply [map { $_->TO_JSON } sort { $a->path cmp $b->path } @errors], [map { $_->TO_JSON } sort { $a->path cmp $b->path } @expected], $descr or Test::More::diag(Mojo::JSON::encode_json(\@errors)); } { $schema->{properties}{v}{format} = 'byte'; validate_ok {v => 'amh0aG9yc2Vu'}, $schema; validate_ok {v => "\0"}, $schema, E('/v', 'Does not match byte format.'); } { $schema->{properties}{v}{format} = 'date'; validate_ok {v => '2014-12-09'}, $schema; validate_ok {v => '0000-00-00'}, $schema, E('/v', 'Month out of range.'); validate_ok {v => '0000-01-00'}, $schema, E('/v', 'Day out of range.'); validate_ok {v => '2014-12-09T20:49:37Z'}, $schema, E('/v', 'Does not match date format.'); validate_ok {v => '0-0-0'}, $schema, E('/v', 'Does not match date format.'); validate_ok {v => '09-12-2014'}, $schema, E('/v', 'Does not match date format.'); validate_ok {v => '09-DEC-2014'}, $schema, E('/v', 'Does not match date format.'); validate_ok {v => '09/12/2014'}, $schema, E('/v', 'Does not match date format.'); } { $schema->{properties}{v}{format} = 'date-time'; validate_ok {v => '2014-12-09T20:49:37Z'}, $schema; validate_ok {v => '0000-00-00T00:00:00Z'}, $schema, E('/v', 'Month out of range.'); validate_ok {v => '0000-01-00T00:00:00Z'}, $schema, E('/v', 'Day out of range.'); validate_ok {v => '20:46:02'}, $schema, E('/v', 'Does not match date-time format.'); } { local $schema->{properties}{v}{type} = 'number'; local $schema->{properties}{v}{format} = 'double'; local $TODO = "cannot test double, since input is already rounded"; validate_ok {v => 1.1000000238418599085576943252817727625370025634765626}, $schema; } { local $schema->{properties}{v}{format} = 'email'; validate_ok {v => 'jhthorsen@cpan.org'}, $schema; validate_ok {v => 'foo'}, $schema, E('/v', 'Does not match email format.'); } { local $schema->{properties}{v}{type} = 'number'; local $schema->{properties}{v}{format} = 'float'; validate_ok {v => -1.10000002384186}, $schema; validate_ok {v => 1.10000002384186}, $schema; local $TODO = 'No idea how to test floats'; validate_ok {v => 0.10000000000000}, $schema, E('/v', 'Does not match float format.'); } { local $TODO = eval 'require Data::Validate::IP;1' ? undef : 'Missing module'; local $schema->{properties}{v}{format} = 'ipv4'; validate_ok {v => '255.100.30.1'}, $schema; validate_ok {v => '300.0.0.0'}, $schema, E('/v', 'Does not match ipv4 format.'); } { local $schema->{properties}{v}{type} = 'integer'; local $schema->{properties}{v}{format} = 'int32'; validate_ok {v => -2147483648}, $schema; validate_ok {v => 2147483647}, $schema; validate_ok {v => 2147483648}, $schema, E('/v', 'Does not match int32 format.'); } if (JSON::Validator::Formats::IV_SIZE >= 8) { local $schema->{properties}{v}{type} = 'integer'; local $schema->{properties}{v}{format} = 'int64'; validate_ok {v => -9223372036854775808}, $schema; validate_ok {v => 9223372036854775807}, $schema; validate_ok {v => 9223372036854775808}, $schema, E('/v', 'Does not match int64 format.'); } { local $schema->{properties}{v}{format} = 'password'; validate_ok {v => 'whatever'}, $schema; } { local $schema->{properties}{v}{format} = 'unknown'; validate_ok {v => 'whatever'}, $schema; } done_testing; Mojolicious-Plugin-OpenAPI-5.11/t/plugin-security-rules-not-defined.t0000644000175100001660000000205014766301334025105 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; post '/global' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 'checks disabled'}); }, 'global'; plugin OpenAPI => {url => 'data://main/sec.json'}; my $t = Test::Mojo->new; $t->post_ok('/api/global' => json => {})->status_is(200)->json_is('/ok' => 'checks disabled'); done_testing; __DATA__ @@ sec.json { "swagger": "2.0", "info": { "version": "0.8", "title": "Pets" }, "schemes": [ "http" ], "basePath": "/api", "securityDefinitions": { "fail1": { "type": "apiKey", "name": "Authorization", "in": "header", "description": "fail1" } }, "security": [{"fail1": []}], "paths": { "/global": { "post": { "x-mojo-name": "global", "parameters": [ { "in": "body", "name": "body", "schema": { "type": "object" } } ], "responses": { "200": {"description": "Echo response", "schema": { "type": "object" }} } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/v2-readonly.t0000644000175100001660000000253214766301334020567 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; post '/user' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'createUser'; plugin OpenAPI => {url => 'data://main/readonly.json'}; my $t = Test::Mojo->new; $t->post_ok('/api/user', json => {age => 42})->status_is(400) ->json_is('/errors/0', {message => 'Read-only.', path => '/body/age'}); $t->post_ok('/api/user', json => {something => 'else'})->status_is(500) ->json_is('/errors/0', {message => 'Missing property.', path => '/body/age'}); done_testing; __DATA__ @@ readonly.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Test readonly" }, "schemes" : [ "http" ], "basePath" : "/api", "paths" : { "/user" : { "post" : { "operationId" : "createUser", "parameters" : [ { "name":"body", "in":"body", "schema": { "$ref": "#/definitions/User" } } ], "responses" : { "200": { "description": "ok", "schema": { "$ref": "#/definitions/User" } } } } } }, "definitions": { "User": { "type" : "object", "required": ["age"], "properties": { "age": { "type": "integer", "readOnly": true } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/v3-body.t0000644000175100001660000000762414766301334017717 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; post '/test' => sub { my $c = shift; $c->openapi->valid_input or return; $c->render(json => undef, status => 200); }, 'test'; post '/test/string' => sub { my $c = shift; $c->openapi->valid_input or return; $c->render(text => $c->req->body, status => 200); }, 'string'; post '/test/optional/explicitly' => sub { my $c = shift; $c->openapi->valid_input or return; $c->render(openapi => undef, status => 200); }, 'test2'; post '/test/optional/implicitly' => sub { my $c = shift; $c->openapi->valid_input or return; $c->render(openapi => undef, status => 200); }, 'test3'; plugin OpenAPI => {url => 'data:///api.yml'}; my $t = Test::Mojo->new(); note 'Valid request should be ok'; $t->post_ok('/test', json => {foo => 'bar'})->status_is(200); note 'Missing property should fail'; $t->post_ok('/test', json => {})->status_is(400)->json_is('/errors/0/message', 'Missing property.'); note 'Array should fail'; $t->post_ok('/test', json => [])->status_is(400) ->json_is('/errors/0/message', 'Expected object - got array.'); note 'Null should fail'; $t->post_ok('/test', json => undef)->status_is(400) ->json_is('/errors/0/message', 'Expected object - got null.'); note 'Invalid JSON should fail'; $t->post_ok('/test', {'Content-Type' => 'application/json'} => 'invalid_json')->status_is(400) ->json_is('/errors/0/message', 'Expected object - got string.'); note 'Invalid Content-Type should fail'; $t->post_ok('/test', {'Content-Type' => 'application/xml'} => '') ->status_is(400) ->json_is('/errors/0/message', 'Expected application/json - got application/xml.'); note 'empty requestBody with "required: false"'; $t->post_ok('/test/optional/explicitly')->status_is(200); note 'requestBody with "required: false"'; $t->post_ok('/test/optional/explicitly', json => {foo => 'bar'})->status_is(200); note 'empty requestBody without "required: false"'; $t->post_ok('/test/optional/implicitly')->status_is(200); note 'requestBody without "required: false"'; $t->post_ok('/test/optional/implicitly', json => {foo => 'bar'})->status_is(200); note 'requestBody as plain string'; $t->post_ok('/test/string', {'Content-Type' => 'text/plain'}, 'cool beans')->status_is(200) ->content_is('cool beans'); note 'not really the first, since "content" is an object, but predictable when there is only one'; $t->post_ok('/test/string', 'first content-type')->status_is(200)->content_is('first content-type'); done_testing; __DATA__ @@ api.yml openapi: 3.0.0 info: title: Test version: 0.0.0 paths: /test: post: x-mojo-name: test requestBody: required: true content: application/json: schema: type: object properties: foo: type: string required: - foo responses: '200': description: ok /test/string: post: x-mojo-name: string requestBody: required: true content: text/plain: schema: type: string responses: '200': description: ok /test/optional/explicitly: post: x-mojo-name: test2 requestBody: required: false content: application/json: schema: type: object properties: foo: type: string required: - foo responses: '200': description: ok /test/optional/implicitly: post: x-mojo-name: test3 requestBody: content: application/json: schema: type: object properties: foo: type: string required: - foo responses: '200': description: ok Mojolicious-Plugin-OpenAPI-5.11/t/plugin-spec-renderer-doc.t0000644000175100001660000000611714766301334023225 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious; sub VERSION {1.42} { my $app = Mojolicious->new; my $under = $app->routes->under('/my-api' => sub {1}); add_routes($app, 'cool_spec_path'); $app->plugin( OpenAPI => {route => $under, url => 'data://main/reply.json', version_from_class => 'main'}); my $t = Test::Mojo->new($app); $t->get_ok('/url')->status_is(200)->content_is('/my-api'); $t->get_ok('/my-api')->status_is(200)->json_is('/basePath', '/my-api') ->json_unlike('/host', qr{api\.thorsen\.pm})->json_like('/host', qr{.:\d+$}) ->json_is('/info/version', 1.42); } { my $app = Mojolicious->new; add_routes($app, 'my.cool.api'); $app->plugin( OpenAPI => { spec_route_name => 'my.cool.api', url => 'data://main/reply.json', version_from_class => 'main' } ); my $t = Test::Mojo->new($app); $t->get_ok('/url')->status_is(200)->content_is('/api'); $t->get_ok('/api')->status_is(200)->json_is('/info/version', 1.42); $t->get_ok('/api.html')->status_is(200)->text_is('title', 'Test reply spec') ->text_is('h1#title', 'Test reply spec')->text_is('h3#op-post--pets a', 'addPet'); $t->get_ok('/api/docs')->status_is(200)->json_is('/info/version', 1.42) ->json_is('/basePath', '/api'); $t->get_ok('/api/docs.html')->status_is(200)->text_is('h3#op-post--pets a', 'addPet') ->text_like('style', qr{font-family:}s) ->text_like('script', qr{SpecRenderer\.prototype\.jsonhtmlify}s) ->content_like(qr{new SpecRenderer\(\).setup\(\)}); SKIP: { skip 'Text::Markdown is not installed', 2 unless eval 'require Text::Markdown;1'; $t->text_is('div.spec-description p', 'pet response') ->text_is('div.spec-description code', 'markdown'); } } sub add_routes { my ($app, $name) = @_; $app->routes->get('/url' => sub { $_[0]->render(text => $_[0]->url_for($name)) }); $app->routes->get( '/docs', [format => [qw(html json)]], {format => undef}, sub { shift->openapi->render_spec } )->name('docs'); $app->routes->post('/pets', sub { shift->render(openapi => {}) })->name('addPet'); return $app; } done_testing; __DATA__ @@ reply.json { "swagger": "2.0", "info": { "version": "0", "title": "Test reply spec" }, "consumes": [ "application/json" ], "produces": [ "application/json" ], "x-mojo-name": "cool_spec_path", "schemes": [ "http" ], "host": "api.thorsen.pm", "basePath": "/api", "paths": { "/docs": { "get": { "operationId": "docs", "responses": { "200": { "description": "pet response\n\nwith `markdown` content", "schema": { "type": "object" } } } } }, "/pets": { "$ref": "#/x-path/Pets" } }, "x-path": { "Pets": { "post": { "operationId": "addPet", "parameters": [ { "in": "body", "name": "body", "schema": { "type": "object" } } ], "responses": { "200": { "description": "pet response", "schema": { "type": "object" } } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/v3-writeonly.t0000644000175100001660000000236514766301334021013 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; post '/required' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {id => 1}, status => 201); }, 'with_required'; plugin OpenAPI => {url => 'data:///schema.json'}; my $t = Test::Mojo->new; $t->post_ok('/required' => json => {app_id => 1})->status_is(201); done_testing; sub post_test { } __DATA__ @@ schema.json { "openapi": "3.0.3", "info": { "title": "Test", "version": "0.0.0" }, "paths": { "/required": { "post": { "operationId": "with_required", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Required" } } } }, "responses": { "201": { "description": "ok", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Required" } } } } } } } }, "components": { "schemas": { "Required": { "required": ["app_id", "id"], "properties": { "app_id": { "type": "integer", "writeOnly": true }, "id": { "type": "integer", "readOnly": true } } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/plugin-security-v2.t0000644000175100001660000002410314766301334022113 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; post '/global' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 1}); }, 'global'; post('/fail_escape' => sub { shift->render(openapi => {ok => 1}) }, 'fail_escape'); post '/simple' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 1}); }, 'simple'; options '/options' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 1}); }, 'options'; post '/fail_or_pass' => sub { my $c = shift->openapi->valid_input or return; die 'Could not connect to dummy database error message' if $ENV{DUMMY_DB_ERROR}; $c->render(openapi => {ok => 1}); }, 'fail_or_pass'; post '/fail_and_pass' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 1}); }, 'fail_and_pass'; post '/multiple_fail' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 1}); }, 'multiple_fail'; post '/multiple_and_fail' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 1}); }, 'multiple_and_fail'; post '/cache' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 1}); }, 'cache'; post '/die' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {ok => 1}); }, 'die'; our %checks; plugin OpenAPI => { url => 'data://main/sec.json', security => { pass1 => sub { my ($c, $def, $scopes, $cb) = @_; $checks{pass1}++; $c->$cb; }, pass2 => sub { my ($c, $def, $scopes, $cb) = @_; $checks{pass2}++; $c->$cb; }, fail1 => sub { my ($c, $def, $scopes, $cb) = @_; $checks{fail1}++; # This deferment causes multiple_and_fail to report # out of order unless order is carefully maintained Mojo::IOLoop->next_tick(sub { $c->$cb('Failed fail1') }); }, fail2 => sub { my ($c, $def, $scopes, $cb) = @_; $checks{fail2}++; my %res = %$def; $res{message} = 'Failed fail2'; $c->$cb(\%res); }, '~fail/escape' => sub { my ($c, $def, $scopes, $cb) = @_; $checks{'~fail/escape'}++; $c->$cb('Failed ~fail/escape'); }, die => sub { my ($c, $def, $scopes, $cb) = @_; $checks{die}++; die 'Argh!'; }, }, }; my %security_definition = (description => 'fail2', in => 'header', name => 'Authorization', type => 'apiKey'); my $t = Test::Mojo->new; { local %checks; $t->post_ok('/api/global' => json => {})->status_is(200)->json_is('/ok' => 1); is_deeply \%checks, {pass1 => 1}, 'expected checks occurred'; } { # global does not define an options handler, so it gets the default # which is allowed through the security local %checks; $t->options_ok('/api/global')->status_is(200); is_deeply \%checks, {}, 'expected checks occurred'; } { local %checks; $t->post_ok('/api/simple' => json => {})->status_is(200)->json_is('/ok' => 1); is_deeply \%checks, {pass2 => 1}, 'expected checks occurred'; } { # route defined with an options handler so it must use the defined security local %checks; $t->options_ok('/api/options' => json => {})->status_is(200)->json_is('/ok' => 1); is_deeply \%checks, {pass1 => 1}, 'expected checks occurred'; } { local %checks; $t->post_ok('/api/fail_or_pass' => json => {})->status_is(200)->json_is('/ok' => 1); is_deeply \%checks, {fail1 => 1, pass1 => 1}, 'expected checks occurred'; } { local $ENV{DUMMY_DB_ERROR} = 1; $t->post_ok('/api/fail_or_pass' => json => {})->status_is(500) ->json_is('/errors/0/message', 'Internal Server Error.')->json_is('/errors/0/path', '/'); } { local %checks; $t->post_ok('/api/fail_and_pass' => json => {})->status_is(401) ->json_is( {errors => [{message => 'Failed fail1', path => '/security/0/fail1'}], status => 401}); is_deeply \%checks, {fail1 => 1, pass1 => 1}, 'expected checks occurred'; } { local %checks; $t->post_ok('/api/multiple_fail' => json => {})->status_is(401)->json_is({ status => 401, errors => [ {message => 'Failed fail1', path => '/security/0/fail1'}, {message => 'Failed fail2', %security_definition}, ] }); is_deeply \%checks, {fail1 => 1, fail2 => 1}, 'expected checks occurred'; } { local %checks; $t->post_ok('/api/multiple_and_fail' => json => {})->status_is(401)->json_is({ status => 401, errors => [ {message => 'Failed fail1', path => '/security/0/fail1'}, {message => 'Failed fail2', %security_definition} ] }); is_deeply \%checks, {fail1 => 1, fail2 => 1}, 'expected checks occurred'; } { local %checks; $t->post_ok('/api/fail_escape' => json => {})->status_is(401)->json_is( { errors => [{message => 'Failed ~fail/escape', path => '/security/0/~0fail~1escape'}], status => 401 } ); is_deeply \%checks, {'~fail/escape' => 1}, 'expected checks occurred'; } { local %checks; $t->post_ok('/api/cache' => json => {})->status_is(200)->json_is('/ok' => 1); is_deeply \%checks, {fail1 => 1, pass1 => 1, pass2 => 1}, 'expected checks occurred'; } { local %checks; $t->post_ok('/api/die' => json => {})->status_is(500)->json_has('/errors/0/message'); is_deeply \%checks, {die => 1}, 'expected checks occurred'; } done_testing; __DATA__ @@ sec.json { "swagger": "2.0", "info": { "version": "0.8", "title": "Pets" }, "schemes": [ "http" ], "basePath": "/api", "securityDefinitions": { "pass1": { "type": "apiKey", "name": "Authorization", "in": "header", "description": "pass1" }, "pass2": { "type": "apiKey", "name": "Authorization", "in": "header", "description": "pass2" }, "fail1": { "type": "apiKey", "name": "Authorization", "in": "header", "description": "fail1" }, "fail2": { "type": "apiKey", "name": "Authorization", "in": "header", "description": "fail2" }, "~fail/escape": { "type": "apiKey", "name": "Authorization", "in": "header", "description": "dummy" }, "die": { "type": "apiKey", "name": "Authorization", "in": "header", "description": "die" } }, "security": [{"pass1": []}], "paths": { "/global": { "post": { "x-mojo-name": "global", "parameters": [ { "in": "body", "name": "body", "schema": { "type": "object" } } ], "responses": { "200": {"description": "Echo response", "schema": { "type": "object" }} } } }, "/simple": { "post": { "x-mojo-name": "simple", "security": [{"pass2": []}], "parameters": [ { "in": "body", "name": "body", "schema": { "type": "object" } } ], "responses": { "200": {"description": "Echo response", "schema": { "type": "object" }} } } }, "/options": { "options": { "x-mojo-name": "options", "security": [{"pass1": []}], "parameters": [ { "in": "body", "name": "body", "schema": { "type": "object" } } ], "responses": { "200": {"description": "Echo response", "schema": { "type": "object" }} } } }, "/fail_or_pass": { "post": { "x-mojo-name": "fail_or_pass", "security": [ {"fail1": []}, {"pass1": []} ], "parameters": [ { "in": "body", "name": "body", "schema": { "type": "object" } } ], "responses": { "200": {"description": "Echo response", "schema": { "type": "object" }} } } }, "/fail_and_pass": { "post": { "x-mojo-name": "fail_and_pass", "security": [ { "fail1": [], "pass1": [] } ], "parameters": [ { "in": "body", "name": "body", "schema": { "type": "object" } } ], "responses": { "200": {"description": "Echo response", "schema": { "type": "object" }} } } }, "/multiple_fail": { "post": { "x-mojo-name": "multiple_fail", "security": [ { "fail1": [] }, { "fail2": [] } ], "parameters": [ { "in": "body", "name": "body", "schema": { "type": "object" } } ], "responses": { "200": {"description": "Echo response", "schema": { "type": "object" }} } } }, "/multiple_and_fail": { "post": { "x-mojo-name": "multiple_and_fail", "security": [ { "fail1": [], "fail2": [] } ], "parameters": [ { "in": "body", "name": "body", "schema": { "type": "object" } } ], "responses": { "200": {"description": "Echo response", "schema": { "type": "object" }} } } }, "/fail_escape": { "post": { "x-mojo-name": "fail_escape", "security": [{"~fail/escape": []}], "parameters": [ { "in": "body", "name": "body", "schema": { "type": "object" } } ], "responses": { "200": {"description": "Echo response", "schema": { "type": "object" }} } } }, "/cache": { "post": { "x-mojo-name": "cache", "security": [ { "fail1": [], "pass1": [] }, { "pass1": [], "pass2": [] } ], "parameters": [ { "in": "body", "name": "body", "schema": { "type": "object" } } ], "responses": { "200": {"description": "Echo response", "schema": { "type": "object" }} } } }, "/die": { "post": { "x-mojo-name": "die", "security": [ {"die": []}, {"pass1": []} ], "parameters": [ { "in": "body", "name": "body", "schema": { "type": "object" } } ], "responses": { "200": {"description": "Echo response", "schema": { "type": "object" }} } } } } } Mojolicious-Plugin-OpenAPI-5.11/t/basic-custom-renderer.t0000644000175100001660000000436114766301334022624 0ustar runnerdockeruse Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; my $age = 43; get '/user' => sub { my $c = shift->openapi->valid_input or return; die "no age!\n" unless defined $age; $c->render(openapi => {age => $age}); }, 'get_user'; post '/user' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {age => $c->param('age')}); }, 'create_user'; plugin OpenAPI => {renderer => \&custom_openapi_renderer, url => 'data://main/user.json'}; my $t = Test::Mojo->new; $t->get_ok('/api/user')->status_is(200)->json_is('/age', 43)->json_is('/t', $^T); $age = 'invalid output!'; note "age = $age"; $t->get_ok('/api/user')->status_is(500)->json_is('/messages/0/path', '/body/age') ->json_is('/t', $^T); $t->post_ok('/api/user', form => {age => 'invalid input'})->status_is(400) ->json_is('/messages/0/path', '/age')->json_is('/t', $^T); undef $age; note 'age = undef'; $t->get_ok('/api/user')->status_is(500)->json_is('/messages/0/message', 'Internal Server Error.') ->json_is('/exception', "no age!\n")->json_is('/t', $^T); $t->get_ok('/api/nope')->status_is(404)->json_is('/messages/0/message', 'Not Found.') ->json_is('/t', $^T); done_testing; sub custom_openapi_renderer { my ($c, $data) = @_; $data->{messages} = delete $data->{errors} if $data->{errors}; $data->{t} = $^T if ref $data eq 'HASH'; $data->{exception} = $c->stash('exception')->message if $c->stash('exception'); return Mojo::JSON::encode_json($data); } __DATA__ @@ user.json { "swagger": "2.0", "info": { "version": "0.8", "title": "Pets" }, "schemes": [ "http" ], "basePath": "/api", "paths": { "/user": { "get": { "x-mojo-name": "get_user", "responses": { "200": { "description": "User", "schema": { "type": "object", "properties": { "age": { "type": "integer"} } } } } }, "post": { "x-mojo-name": "create_user", "parameters": [ { "in": "formData", "name": "age", "type": "integer" } ], "responses": { "400": { "description": "Error", "schema": { "type": "object" } } } } } } }