Mojolicious-Plugin-OpenAPI-2.21/000755 000765 000024 00000000000 13612462655 017600 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OpenAPI-2.21/cpanfile000644 000765 000024 00000000530 13574517677 021316 0ustar00jhthorsenstaff000000 000000 # You can install this project with curl -L http://cpanmin.us | perl - https://github.com/jhthorsen/mojolicious-plugin-openapi/archive/master.tar.gz requires "Mojolicious" => "8.00"; requires "JSON::Validator" => "3.16"; requires "YAML::XS" => "0.80"; recommends "Text::Markdown" => "1.0.31"; test_requires "Test::More" => "0.88"; Mojolicious-Plugin-OpenAPI-2.21/.vstags000644 000765 000024 00000025540 13523111044 021076 0ustar00jhthorsenstaff000000 000000 !_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ !_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ !_TAG_PROGRAM_AUTHOR Darren Hiebert /dhiebert@users.sourceforge.net/ !_TAG_PROGRAM_NAME Exuberant Ctags // !_TAG_PROGRAM_URL http://ctags.sourceforge.net /official site/ !_TAG_PROGRAM_VERSION 5.8 // DEBUG lib/JSON/Validator/OpenAPI/Mojolicious.pm 9;" c DEBUG lib/Mojolicious/Plugin/OpenAPI.pm 8;" c DEBUG lib/Mojolicious/Plugin/OpenAPI/Cors.pm 4;" c DEBUG lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 6;" c E lib/JSON/Validator/OpenAPI/Mojolicious.pm 20;" s IV_SIZE lib/JSON/Validator/OpenAPI/Mojolicious.pm 10;" c JSON::Validator::OpenAPI::Mojolicious lib/JSON/Validator/OpenAPI/Mojolicious.pm 1;" p JSON::Validator::OpenAPI::Mojolicious::DEBUG lib/JSON/Validator/OpenAPI/Mojolicious.pm 9;" c JSON::Validator::OpenAPI::Mojolicious::E lib/JSON/Validator/OpenAPI/Mojolicious.pm 20;" s JSON::Validator::OpenAPI::Mojolicious::IV_SIZE lib/JSON/Validator/OpenAPI/Mojolicious.pm 10;" c JSON::Validator::OpenAPI::Mojolicious::_build_formats lib/JSON/Validator/OpenAPI/Mojolicious.pm 151;" s JSON::Validator::OpenAPI::Mojolicious::_coerce_by_collection_format lib/JSON/Validator/OpenAPI/Mojolicious.pm 165;" s JSON::Validator::OpenAPI::Mojolicious::_confess_invalid_in lib/JSON/Validator/OpenAPI/Mojolicious.pm 186;" s JSON::Validator::OpenAPI::Mojolicious::_get_request_data lib/JSON/Validator/OpenAPI/Mojolicious.pm 190;" s JSON::Validator::OpenAPI::Mojolicious::_get_request_uploads lib/JSON/Validator/OpenAPI/Mojolicious.pm 217;" s JSON::Validator::OpenAPI::Mojolicious::_get_response_data lib/JSON/Validator/OpenAPI/Mojolicious.pm 222;" s JSON::Validator::OpenAPI::Mojolicious::_match_byte_string lib/JSON/Validator/OpenAPI/Mojolicious.pm 228;" s JSON::Validator::OpenAPI::Mojolicious::_match_number lib/JSON/Validator/OpenAPI/Mojolicious.pm 230;" s JSON::Validator::OpenAPI::Mojolicious::_negotiate_accept_header lib/JSON/Validator/OpenAPI/Mojolicious.pm 239;" s JSON::Validator::OpenAPI::Mojolicious::_resolve_ref lib/JSON/Validator/OpenAPI/Mojolicious.pm 270;" s JSON::Validator::OpenAPI::Mojolicious::_set_request_data lib/JSON/Validator/OpenAPI/Mojolicious.pm 276;" s JSON::Validator::OpenAPI::Mojolicious::_to_list lib/JSON/Validator/OpenAPI/Mojolicious.pm 301;" s JSON::Validator::OpenAPI::Mojolicious::_validate_request_body lib/JSON/Validator/OpenAPI/Mojolicious.pm 305;" s JSON::Validator::OpenAPI::Mojolicious::_validate_request_value lib/JSON/Validator/OpenAPI/Mojolicious.pm 320;" s JSON::Validator::OpenAPI::Mojolicious::_validate_response_headers lib/JSON/Validator/OpenAPI/Mojolicious.pm 354;" s JSON::Validator::OpenAPI::Mojolicious::_validate_type_array lib/JSON/Validator/OpenAPI/Mojolicious.pm 379;" s JSON::Validator::OpenAPI::Mojolicious::_validate_type_file lib/JSON/Validator/OpenAPI/Mojolicious.pm 389;" s JSON::Validator::OpenAPI::Mojolicious::_validate_type_object lib/JSON/Validator/OpenAPI/Mojolicious.pm 396;" s JSON::Validator::OpenAPI::Mojolicious::load_and_validate_schema lib/JSON/Validator/OpenAPI/Mojolicious.pm 22;" s JSON::Validator::OpenAPI::Mojolicious::validate_input lib/JSON/Validator/OpenAPI/Mojolicious.pm 50;" s JSON::Validator::OpenAPI::Mojolicious::validate_request lib/JSON/Validator/OpenAPI/Mojolicious.pm 57;" s JSON::Validator::OpenAPI::Mojolicious::validate_response lib/JSON/Validator/OpenAPI/Mojolicious.pm 122;" s MARKDOWN lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 7;" c Mojolicious::Plugin::OpenAPI lib/Mojolicious/Plugin/OpenAPI.pm 1;" p Mojolicious::Plugin::OpenAPI::Cors lib/Mojolicious/Plugin/OpenAPI/Cors.pm 1;" p Mojolicious::Plugin::OpenAPI::Cors::DEBUG lib/Mojolicious/Plugin/OpenAPI/Cors.pm 4;" c Mojolicious::Plugin::OpenAPI::Cors::_add_preflighted_routes lib/Mojolicious/Plugin/OpenAPI/Cors.pm 41;" s Mojolicious::Plugin::OpenAPI::Cors::_default_cors_exchange_callback lib/Mojolicious/Plugin/OpenAPI/Cors.pm 58;" s Mojolicious::Plugin::OpenAPI::Cors::_exchange lib/Mojolicious/Plugin/OpenAPI/Cors.pm 66;" s Mojolicious::Plugin::OpenAPI::Cors::_is_preflighted_request lib/Mojolicious/Plugin/OpenAPI/Cors.pm 96;" s Mojolicious::Plugin::OpenAPI::Cors::_is_simple_request lib/Mojolicious/Plugin/OpenAPI/Cors.pm 110;" s Mojolicious::Plugin::OpenAPI::Cors::_render_bad_request lib/Mojolicious/Plugin/OpenAPI/Cors.pm 124;" s Mojolicious::Plugin::OpenAPI::Cors::_set_default_headers lib/Mojolicious/Plugin/OpenAPI/Cors.pm 134;" s Mojolicious::Plugin::OpenAPI::Cors::_takeover_exchange_route lib/Mojolicious/Plugin/OpenAPI/Cors.pm 161;" s Mojolicious::Plugin::OpenAPI::Cors::register lib/Mojolicious/Plugin/OpenAPI/Cors.pm 17;" s Mojolicious::Plugin::OpenAPI::DEBUG lib/Mojolicious/Plugin/OpenAPI.pm 8;" c Mojolicious::Plugin::OpenAPI::Security lib/Mojolicious/Plugin/OpenAPI/Security.pm 1;" p Mojolicious::Plugin::OpenAPI::Security::_build_action lib/Mojolicious/Plugin/OpenAPI/Security.pm 15;" s Mojolicious::Plugin::OpenAPI::Security::_pointer_escape lib/Mojolicious/Plugin/OpenAPI/Security.pm 96;" s Mojolicious::Plugin::OpenAPI::Security::register lib/Mojolicious/Plugin/OpenAPI/Security.pm 6;" s Mojolicious::Plugin::OpenAPI::SpecRenderer lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 1;" p Mojolicious::Plugin::OpenAPI::SpecRenderer::DEBUG lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 6;" c Mojolicious::Plugin::OpenAPI::SpecRenderer::MARKDOWN lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 7;" c Mojolicious::Plugin::OpenAPI::SpecRenderer::_add_documentation_routes lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 26;" s Mojolicious::Plugin::OpenAPI::SpecRenderer::_markdown lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 43;" s Mojolicious::Plugin::OpenAPI::SpecRenderer::_render_partial_spec lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 47;" s Mojolicious::Plugin::OpenAPI::SpecRenderer::_render_spec lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 77;" s Mojolicious::Plugin::OpenAPI::SpecRenderer::_serialize lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 100;" s Mojolicious::Plugin::OpenAPI::SpecRenderer::register lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 9;" s Mojolicious::Plugin::OpenAPI::_add_default_response lib/Mojolicious/Plugin/OpenAPI.pm 68;" s Mojolicious::Plugin::OpenAPI::_add_routes lib/Mojolicious/Plugin/OpenAPI.pm 94;" s Mojolicious::Plugin::OpenAPI::_before_render lib/Mojolicious/Plugin/OpenAPI.pm 148;" s Mojolicious::Plugin::OpenAPI::_build_route lib/Mojolicious/Plugin/OpenAPI.pm 174;" s Mojolicious::Plugin::OpenAPI::_default_schema lib/Mojolicious/Plugin/OpenAPI.pm 199;" s Mojolicious::Plugin::OpenAPI::_default_schema_v2 lib/Mojolicious/Plugin/OpenAPI.pm 216;" s Mojolicious::Plugin::OpenAPI::_default_schema_v3 lib/Mojolicious/Plugin/OpenAPI.pm 221;" s Mojolicious::Plugin::OpenAPI::_helper_get_spec lib/Mojolicious/Plugin/OpenAPI.pm 229;" s Mojolicious::Plugin::OpenAPI::_helper_reply lib/Mojolicious/Plugin/OpenAPI.pm 246;" s Mojolicious::Plugin::OpenAPI::_helper_validate lib/Mojolicious/Plugin/OpenAPI.pm 269;" s Mojolicious::Plugin::OpenAPI::_log lib/Mojolicious/Plugin/OpenAPI.pm 292;" s Mojolicious::Plugin::OpenAPI::_openapi_path_to_route_path lib/Mojolicious/Plugin/OpenAPI.pm 333;" s Mojolicious::Plugin::OpenAPI::_parameters_for lib/Mojolicious/Plugin/OpenAPI.pm 304;" s Mojolicious::Plugin::OpenAPI::_render lib/Mojolicious/Plugin/OpenAPI.pm 306;" s Mojolicious::Plugin::OpenAPI::_self lib/Mojolicious/Plugin/OpenAPI.pm 346;" s Mojolicious::Plugin::OpenAPI::register lib/Mojolicious/Plugin/OpenAPI.pm 26;" s _add_default_response lib/Mojolicious/Plugin/OpenAPI.pm 68;" s _add_documentation_routes lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 26;" s _add_preflighted_routes lib/Mojolicious/Plugin/OpenAPI/Cors.pm 41;" s _add_routes lib/Mojolicious/Plugin/OpenAPI.pm 94;" s _before_render lib/Mojolicious/Plugin/OpenAPI.pm 148;" s _build_action lib/Mojolicious/Plugin/OpenAPI/Security.pm 15;" s _build_formats lib/JSON/Validator/OpenAPI/Mojolicious.pm 151;" s _build_route lib/Mojolicious/Plugin/OpenAPI.pm 174;" s _coerce_by_collection_format lib/JSON/Validator/OpenAPI/Mojolicious.pm 165;" s _confess_invalid_in lib/JSON/Validator/OpenAPI/Mojolicious.pm 186;" s _default_cors_exchange_callback lib/Mojolicious/Plugin/OpenAPI/Cors.pm 58;" s _default_schema lib/Mojolicious/Plugin/OpenAPI.pm 199;" s _default_schema_v2 lib/Mojolicious/Plugin/OpenAPI.pm 216;" s _default_schema_v3 lib/Mojolicious/Plugin/OpenAPI.pm 221;" s _exchange lib/Mojolicious/Plugin/OpenAPI/Cors.pm 66;" s _get_request_data lib/JSON/Validator/OpenAPI/Mojolicious.pm 190;" s _get_request_uploads lib/JSON/Validator/OpenAPI/Mojolicious.pm 217;" s _get_response_data lib/JSON/Validator/OpenAPI/Mojolicious.pm 222;" s _helper_get_spec lib/Mojolicious/Plugin/OpenAPI.pm 229;" s _helper_reply lib/Mojolicious/Plugin/OpenAPI.pm 246;" s _helper_validate lib/Mojolicious/Plugin/OpenAPI.pm 269;" s _is_preflighted_request lib/Mojolicious/Plugin/OpenAPI/Cors.pm 96;" s _is_simple_request lib/Mojolicious/Plugin/OpenAPI/Cors.pm 110;" s _log lib/Mojolicious/Plugin/OpenAPI.pm 292;" s _markdown lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 43;" s _match_byte_string lib/JSON/Validator/OpenAPI/Mojolicious.pm 228;" s _match_number lib/JSON/Validator/OpenAPI/Mojolicious.pm 230;" s _negotiate_accept_header lib/JSON/Validator/OpenAPI/Mojolicious.pm 239;" s _openapi_path_to_route_path lib/Mojolicious/Plugin/OpenAPI.pm 333;" s _parameters_for lib/Mojolicious/Plugin/OpenAPI.pm 304;" s _pointer_escape lib/Mojolicious/Plugin/OpenAPI/Security.pm 96;" s _render lib/Mojolicious/Plugin/OpenAPI.pm 306;" s _render_bad_request lib/Mojolicious/Plugin/OpenAPI/Cors.pm 124;" s _render_partial_spec lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 47;" s _render_spec lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 77;" s _resolve_ref lib/JSON/Validator/OpenAPI/Mojolicious.pm 270;" s _self lib/Mojolicious/Plugin/OpenAPI.pm 346;" s _serialize lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 100;" s _set_default_headers lib/Mojolicious/Plugin/OpenAPI/Cors.pm 134;" s _set_request_data lib/JSON/Validator/OpenAPI/Mojolicious.pm 276;" s _takeover_exchange_route lib/Mojolicious/Plugin/OpenAPI/Cors.pm 161;" s _to_list lib/JSON/Validator/OpenAPI/Mojolicious.pm 301;" s _validate_request_body lib/JSON/Validator/OpenAPI/Mojolicious.pm 305;" s _validate_request_value lib/JSON/Validator/OpenAPI/Mojolicious.pm 320;" s _validate_response_headers lib/JSON/Validator/OpenAPI/Mojolicious.pm 354;" s _validate_type_array lib/JSON/Validator/OpenAPI/Mojolicious.pm 379;" s _validate_type_file lib/JSON/Validator/OpenAPI/Mojolicious.pm 389;" s _validate_type_object lib/JSON/Validator/OpenAPI/Mojolicious.pm 396;" s load_and_validate_schema lib/JSON/Validator/OpenAPI/Mojolicious.pm 22;" s register lib/Mojolicious/Plugin/OpenAPI.pm 26;" s register lib/Mojolicious/Plugin/OpenAPI/Cors.pm 17;" s register lib/Mojolicious/Plugin/OpenAPI/Security.pm 6;" s register lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm 9;" s validate_input lib/JSON/Validator/OpenAPI/Mojolicious.pm 50;" s validate_request lib/JSON/Validator/OpenAPI/Mojolicious.pm 57;" s validate_response lib/JSON/Validator/OpenAPI/Mojolicious.pm 122;" s Mojolicious-Plugin-OpenAPI-2.21/Changes000644 000765 000024 00000024644 13612462654 021104 0ustar00jhthorsenstaff000000 000000 Revision history for perl distribution Mojolicious-Plugin-OpenAPI 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-2.21/MANIFEST000644 000765 000024 00000003205 13612462655 020731 0ustar00jhthorsenstaff000000 000000 .perltidyrc .travis.yml .vstags Changes cpanfile lib/JSON/Validator/OpenAPI/Mojolicious.pm lib/Mojolicious/Plugin/OpenAPI.pm lib/Mojolicious/Plugin/OpenAPI/Cors.pm lib/Mojolicious/Plugin/OpenAPI/Guides/OpenAPI3.pod lib/Mojolicious/Plugin/OpenAPI/Guides/Swagger2.pod lib/Mojolicious/Plugin/OpenAPI/Guides/Tutorial.pod lib/Mojolicious/Plugin/OpenAPI/Security.pm lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm Makefile.PL MANIFEST This list of files README.md t/00-basic.t t/authenticate.t t/autorender.t t/bundle.t t/coerce.t t/collectionformat.t t/correct-order-of-paths.t t/cors.t t/custom-format.t t/data/image.jpeg t/default-value.t t/discriminator.t t/empty-string.t t/error-messages.t t/example-array-of-hashes.t t/headers.t t/invalid-json.t t/issue-24-booleans-in-yaml-schema.t t/issue-48-refs.t t/load-and-validate-spec.t t/load-from-app.t t/path-parameters.t t/recursion.t t/ref-param.t t/register.t t/renderer.t t/reply-spec.t t/route-names.t t/security-disabled.t t/set-request.t t/spec.t t/spec/bundlecheck.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-valid_file_refs.yaml t/spec/v3-valid_include.yaml t/swagger2.t t/tutorial.t t/tutorial_v3.t t/v2-file.t t/v2-formats.t t/v2-readonly.t t/v2-security.t t/v3-body.t t/v3-default.t t/v3-invalid_file_refs.t t/v3-invalid_file_refs_no_path.t t/v3-nullable.t t/v3-security.t t/v3-style-array.t t/v3-valid_file_refs.t t/v3.t t/validate.t t/x-mojo-placeholder.t t/yaml.t META.yml Module YAML meta-data (added by MakeMaker) META.json Module JSON meta-data (added by MakeMaker) Mojolicious-Plugin-OpenAPI-2.21/t/000755 000765 000024 00000000000 13612462655 020043 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OpenAPI-2.21/README.md000644 000765 000024 00000023477 13612462654 021073 0ustar00jhthorsenstaff000000 000000 # NAME Mojolicious::Plugin::OpenAPI - OpenAPI / Swagger plugin for Mojolicious # SYNOPSIS use Mojolicious::Lite; # 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->validation->param("body")}; # 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:///spec.json"}; app->start; __DATA__ @@ spec.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Echo Service" }, "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" } } } } } } } See [Mojolicious::Plugin::OpenAPI::Guides::Tutorial](https://metacpan.org/pod/Mojolicious::Plugin::OpenAPI::Guides::Tutorial) for a tutorial on how to write a "full" app with application class and controllers. # DESCRIPTION [Mojolicious::Plugin::OpenAPI](https://metacpan.org/pod/Mojolicious::Plugin::OpenAPI) is [Mojolicious::Plugin](https://metacpan.org/pod/Mojolicious::Plugin) that add routes and input/output validation to your [Mojolicious](https://metacpan.org/pod/Mojolicious) application based on a OpenAPI (Swagger) specification. Have a look at the ["SEE ALSO"](#see-also) for references to more documentation, or jump right to the [tutorial](https://metacpan.org/pod/Mojolicious::Plugin::OpenAPI::Guides::Tutorial). Currently v2 is very well supported, while v3 should be considered EXPERIMENTAL. Please report in [issues](https://github.com/jhthorsen/json-validator/issues) or open pull requests to enhance the 3.0 support. # HELPERS ## 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 `$json_pointer` will be relative to the current operation. Example: { "paths": { "/pets": { "get": { // This datastructure is returned by default } } } } ## openapi.validate @errors = $c->openapi->validate; Used to validate a request. `@errors` holds a list of [JSON::Validator::Error](https://metacpan.org/pod/JSON::Validator::Error) objects or empty list on valid input. Note that this helper is only for customization. You probably want ["openapi.valid\_input"](#openapi-valid_input) in most cases. Validated input parameters will be copied to `Mojolicious::Controller/validation`, which again can be extracted by the "name" in the parameters list from the spec. Example: # specification: "parameters": [{"in": "body", "name": "whatever", "schema": {"type": "object"}}], # controller my $body = $c->validation->param("whatever"); ## openapi.valid\_input $c = $c->openapi->valid_input; Returns the [Mojolicious::Controller](https://metacpan.org/pod/Mojolicious::Controller) object if the input is valid or automatically render an error document if not and return false. See ["SYNOPSIS"](#synopsis) for example usage. # HOOKS [Mojolicious::Plugin::OpenAPI](https://metacpan.org/pod/Mojolicious::Plugin::OpenAPI) will emit the following hooks on the [application](https://metacpan.org/pod/Mojolicious) object. ## 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. # RENDERER This plugin register a new handler called `openapi`. 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 ["renderer"](#renderer) to format the output data. The code below shows the default ["renderer"](#renderer) which generates JSON data: $app->plugin( OpenAPI => { renderer => sub { my ($c, $data) = @_; return Mojo::JSON::encode_json($data); } } ); # ATTRIBUTES ## route $route = $openapi->route; The parent [Mojolicious::Routes::Route](https://metacpan.org/pod/Mojolicious::Routes::Route) object for all the OpenAPI endpoints. ## validator $jv = $openapi->validator; Holds a [JSON::Validator::OpenAPI::Mojolicious](https://metacpan.org/pod/JSON::Validator::OpenAPI::Mojolicious) object. # METHODS ## register $openapi = $openapi->register($app, \%config); $openapi = $app->plugin(OpenAPI => \%config); Loads the OpenAPI specification, validates it and add routes to [$app](https://metacpan.org/pod/Mojolicious). It will also set up ["HELPERS"](#helpers) and adds a [before\_render](https://metacpan.org/pod/Mojolicious#before_render) hook for auto-rendering of error documents. The return value is the object instance, which allow you to access the ["ATTRIBUTES"](#attributes) after you load the plugin. `%config` can have: ### allow\_invalid\_ref The OpenAPI specification does not allow "$ref" at every level, but setting this flag to a true value will ignore the $ref check. Note that setting this attribute is discourage. ### coerce See ["coerce" in JSON::Validator](https://metacpan.org/pod/JSON::Validator#coerce) for possible values that `coerce` can take. Default: booleans,numbers,strings The default value will include "defaults" in the future, once that is stable enough. ### default\_response\_codes A list of response codes that will get a `"$ref"` pointing to "#/definitions/DefaultResponse", unless already defined in the spec. "DefaultResponse" can be altered by setting ["default\_response\_name"](#default_response_name). The default response code list is the following: 400 | Bad Request | Invalid input from client / user agent 401 | Unauthorized | Used by Mojolicious::Plugin::OpenAPI::Security 404 | Not Found | Route is not defined 500 | Internal Server Error | Internal error or failed output validation 501 | Not Implemented | Route exists, but the action is not implemented Note that more default codes might be added in the future if required by the plugin. ### default\_response\_name The name of the "definition" in the spec that will be used for ["default\_response\_codes"](#default_response_codes). The default value is "DefaultResponse". See ["Default response schema" in Mojolicious::Plugin::OpenAPI::Guides::Tutorial](https://metacpan.org/pod/Mojolicious::Plugin::OpenAPI::Guides::Tutorial#Default-response-schema) for more details. ### log\_level `log_level` is used when logging invalid request/response error messages. Default: "warn". ### plugins A list of OpenAPI classes to extend the functionality. Default is: [Mojolicious::Plugin::OpenAPI::Cors](https://metacpan.org/pod/Mojolicious::Plugin::OpenAPI::Cors), [Mojolicious::Plugin::OpenAPI::SpecRenderer](https://metacpan.org/pod/Mojolicious::Plugin::OpenAPI::SpecRenderer) and [Mojolicious::Plugin::OpenAPI::Security](https://metacpan.org/pod/Mojolicious::Plugin::OpenAPI::Security). $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)]}); ### renderer See ["RENDERER"](#renderer). ### route `route` 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"), }); ### schema Can be used to set a different schema, than the default OpenAPI 2.0 spec. Example values: "http://swagger.io/v2/schema.json", "v2" or "v3". ### 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. ### url See ["schema" in JSON::Validator](https://metacpan.org/pod/JSON::Validator#schema) for the different `url` formats that is accepted. `spec` is an alias for "url", which might make more sense if your specification is written in perl, instead of JSON or YAML. ### version\_from\_class Can be used to overridden `/info/version` in the API specification, from the return value from the `VERSION()` method in `version_from_class`. This will only have an effect if "version" is "0". Defaults to the current `$app`. # AUTHORS Henrik Andersen Ilya Rassadin Jan Henning Thorsen Joel Berger # 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. # SEE ALSO - [Mojolicious::Plugin::OpenAPI::Guides::Tutorial](https://metacpan.org/pod/Mojolicious::Plugin::OpenAPI::Guides::Tutorial) - [Mojolicious::Plugin::OpenAPI::Cors](https://metacpan.org/pod/Mojolicious::Plugin::OpenAPI::Cors) - [Mojolicious::Plugin::OpenAPI::Security](https://metacpan.org/pod/Mojolicious::Plugin::OpenAPI::Security) - [Mojolicious::Plugin::OpenAPI::SpecRenderer](https://metacpan.org/pod/Mojolicious::Plugin::OpenAPI::SpecRenderer) - [OpenAPI specification](https://openapis.org/specification) Mojolicious-Plugin-OpenAPI-2.21/META.yml000664 000765 000024 00000001713 13612462655 021055 0ustar00jhthorsenstaff000000 000000 --- abstract: 'OpenAPI / Swagger plugin for Mojolicious' author: - 'Jan Henning Thorsen ' build_requires: Test::More: '0.88' configure_requires: ExtUtils::MakeMaker: '0' dynamic_config: 0 generated_by: 'ExtUtils::MakeMaker version 7.34, 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 requires: JSON::Validator: '3.16' Mojolicious: '8.00' YAML::XS: '0.80' resources: bugtracker: https://github.com/jhthorsen/mojolicious-plugin-openapi/issues homepage: https://github.com/jhthorsen/mojolicious-plugin-openapi repository: https://github.com/jhthorsen/mojolicious-plugin-openapi.git version: '2.21' x_contributors: - 'Henrik Andersen' - 'Ilya Rassadin' - 'Jan Henning Thorsen' - 'Joel Berger' x_serialization_backend: 'CPAN::Meta::YAML version 0.018' Mojolicious-Plugin-OpenAPI-2.21/lib/000755 000765 000024 00000000000 13612462655 020346 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OpenAPI-2.21/Makefile.PL000644 000765 000024 00000003042 13612462654 021550 0ustar00jhthorsenstaff000000 000000 # Generated by git-ship. See 'git-ship --man' for help or https://github.com/jhthorsen/app-git-ship use utf8; use ExtUtils::MakeMaker; 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', EXE_FILES => [qw()], BUILD_REQUIRES => {} , TEST_REQUIRES => { 'Test::More' => '0.88' } , PREREQ_PM => { 'JSON::Validator' => '3.16', 'Mojolicious' => '8.00', 'YAML::XS' => '0.80' } , META_MERGE => { 'dynamic_config' => 0, 'meta-spec' => {version => 2}, 'resources' => { bugtracker => {web => 'https://github.com/jhthorsen/mojolicious-plugin-openapi/issues'}, homepage => 'https://github.com/jhthorsen/mojolicious-plugin-openapi', repository => { type => 'git', url => 'https://github.com/jhthorsen/mojolicious-plugin-openapi.git', web => 'https://github.com/jhthorsen/mojolicious-plugin-openapi', }, }, 'x_contributors' => [ 'Henrik Andersen', 'Ilya Rassadin', 'Jan Henning Thorsen', 'Joel Berger' ] , }, 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-2.21/.perltidyrc000644 000765 000024 00000001011 13551721100 021734 0ustar00jhthorsenstaff000000 000000 -pbp # Start with Perl Best Practices -w # Show all warnings -iob # Ignore old breakpoints -l=100 # Characters per line -mbl=2 # No more than 2 blank lines -i=2 # Indentation is 2 columns -ci=2 # Continuation indentation is 2 columns -vt=0 # Less vertical tightness -pt=2 # High parenthesis tightness -bt=2 # High brace tightness -sbt=2 # High square bracket tightness -isbc # Don't indent comments without leading space -nst # undo -st from -pbp, to allow for command line use Mojolicious-Plugin-OpenAPI-2.21/.travis.yml000644 000765 000024 00000000726 13551721100 021677 0ustar00jhthorsenstaff000000 000000 language: perl matrix: include: - perl: "5.28" dist: xenial - perl: "5.20" dist: trusty - perl: "5.16" dist: trusty - perl: "5.10" dist: trusty env: - "HARNESS_OPTIONS=j6" install: - "cpanm -n Test::Pod Test::Pod::Coverage Data::Validate::Domain Data::Validate::IP YAML::LibYAML" - "cpanm -n https://github.com/mojolicious/json-validator/archive/master.tar.gz" - "cpanm -n --installdeps ." notifications: email: false Mojolicious-Plugin-OpenAPI-2.21/META.json000664 000765 000024 00000003155 13612462655 021227 0ustar00jhthorsenstaff000000 000000 { "abstract" : "OpenAPI / Swagger plugin for Mojolicious", "author" : [ "Jan Henning Thorsen " ], "dynamic_config" : 0, "generated_by" : "ExtUtils::MakeMaker version 7.34, 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" ] }, "prereqs" : { "build" : { "requires" : {} }, "configure" : { "requires" : { "ExtUtils::MakeMaker" : "0" } }, "runtime" : { "requires" : { "JSON::Validator" : "3.16", "Mojolicious" : "8.00", "YAML::XS" : "0.80" } }, "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", "repository" : { "type" : "git", "url" : "https://github.com/jhthorsen/mojolicious-plugin-openapi.git", "web" : "https://github.com/jhthorsen/mojolicious-plugin-openapi" } }, "version" : "2.21", "x_contributors" : [ "Henrik Andersen", "Ilya Rassadin", "Jan Henning Thorsen", "Joel Berger" ], "x_serialization_backend" : "JSON::PP version 4.02" } Mojolicious-Plugin-OpenAPI-2.21/lib/JSON/000755 000765 000024 00000000000 13612462655 021117 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OpenAPI-2.21/lib/Mojolicious/000755 000765 000024 00000000000 13612462655 022642 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OpenAPI-2.21/lib/Mojolicious/Plugin/000755 000765 000024 00000000000 13612462655 024100 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OpenAPI-2.21/lib/Mojolicious/Plugin/OpenAPI.pm000644 000765 000024 00000047701 13612462654 025701 0ustar00jhthorsenstaff000000 000000 package Mojolicious::Plugin::OpenAPI; use Mojo::Base 'Mojolicious::Plugin'; use JSON::Validator::OpenAPI::Mojolicious; use JSON::Validator::Ref; use Mojo::JSON; use Mojo::Util; use constant DEBUG => $ENV{MOJO_OPENAPI_DEBUG} || 0; our $VERSION = '2.21'; my $X_RE = qr{^x-}; has route => sub {undef}; has validator => sub { JSON::Validator::OpenAPI::Mojolicious->new; }; has _renderer => sub { return sub { 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 Mojo::JSON::encode_json($_[0]); }; }; sub register { my ($self, $app, $config) = @_; $self->validator->coerce($config->{coerce} // 'booleans,numbers,strings'); $self->validator->load_and_validate_schema( $config->{url} || $config->{spec}, { allow_invalid_ref => $config->{allow_invalid_ref}, schema => $config->{schema}, version_from_class => $config->{version_from_class} // ref $app, } ); unless ($app->defaults->{'openapi.base_paths'}) { $app->helper('openapi.spec' => \&_helper_get_spec); $app->helper('openapi.valid_input' => sub { _helper_validate($_[0]) ? undef : $_[0] }); $app->helper('openapi.validate' => \&_helper_validate); $app->helper('reply.openapi' => \&_helper_reply); $app->hook(before_render => \&_before_render); $app->renderer->add_handler(openapi => \&_render); } # Removed in 2.00 die "[OpenAPI] default_response is no longer supported in config" if $config->{default_response}; $self->{default_response_codes} = $config->{default_response_codes} || [400, 401, 404, 500, 501]; $self->{default_response_name} = $config->{default_response_name} || 'DefaultResponse'; $self->{log_level} = $ENV{MOJO_OPENAPI_LOG_LEVEL} || $config->{log_level} || 'warn'; $self->_renderer($config->{renderer}) if $config->{renderer}; $self->_build_route($app, $config); my @plugins; 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, $self, $config); } $self->_add_routes($app, $config); $self; } sub _add_default_response { my ($self, $op_spec) = @_; my $name = $self->{default_response_name}; my $schema_data = $self->validator->schema->data; # turn off with config { default_response_codes => [] } return unless @{$self->{default_response_codes}}; my $ref = $self->validator->version ge '3' ? ($schema_data->{components}{schemas}{$name} ||= $self->_default_schema) : ($schema_data->{definitions}{$name} ||= $self->_default_schema); my %schema = $self->validator->version ge '3' ? ('$ref' => "#/components/schemas/$name") : ('$ref' => "#/definitions/$name"); tie %schema, 'JSON::Validator::Ref', $ref, $schema{'$ref'}, $schema{'$ref'}; for my $code (@{$self->{default_response_codes}}) { if ($self->validator->version ge '3') { $op_spec->{responses}{$code} ||= $self->_default_schema_v3(\%schema); } else { $op_spec->{responses}{$code} ||= $self->_default_schema_v2(\%schema); } } } sub _add_routes { my ($self, $app, $config) = @_; my (@routes, %uniq); my @sorted_openapi_paths = map { $_->[0] } sort { $a->[1] <=> $b->[1] || length $a->[0] <=> length $b->[0] } map { [$_, /\{/ ? 1 : 0] } grep { !/$X_RE/ } keys %{$self->validator->get('/paths') || {}}; for my $openapi_path (@sorted_openapi_paths) { my $path_parameters = $self->validator->get([paths => $openapi_path => 'parameters']) || []; for my $http_method (sort keys %{$self->validator->get([paths => $openapi_path]) || {}}) { next if $http_method =~ $X_RE or $http_method eq 'parameters'; my $op_spec = $self->validator->get([paths => $openapi_path => $http_method]); my $name = $op_spec->{'x-mojo-name'} || $op_spec->{operationId}; my $to = $op_spec->{'x-mojo-to'}; my $r; $self->{parameters_for}{$openapi_path}{$http_method} = [@$path_parameters, @{$op_spec->{parameters} || []}]; 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 (!$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 $route_path = $self->_openapi_path_to_route_path($http_method, $openapi_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; } $self->_add_default_response($op_spec); $r->to(ref $to eq 'ARRAY' ? @$to : $to) if $to; $r->to({'openapi.method' => $http_method}); $r->to({'openapi.path' => $openapi_path}); warn "[OpenAPI] Add route $http_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 eq '404' and $c->stash('openapi.path')) ? 501 : $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 $route = $config->{route}; my $base_path = $self->validator->version eq '3' ? Mojo::URL->new($self->validator->get('/servers/0/url') || '/')->path->to_string : $self->validator->get('/basePath') || '/'; $route = $route->any($base_path) if $route and !$route->pattern->unparsed; $route = $app->routes->any($base_path) unless $route; $base_path = $self->validator->schema->data->{basePath} = $route->to_string; $base_path =~ s!/$!!; push @{$app->defaults->{'openapi.base_paths'}}, [$base_path, $self]; $route->to({handler => 'openapi', 'openapi.object' => $self}); if (my $spec_route_name = $config->{spec_route_name} || $self->validator->get('/x-mojo-name')) { $self->{route_prefix} = "$spec_route_name."; } $self->{route_prefix} //= ''; $self->route($route); } sub _default_schema { +{ type => 'object', required => ['errors'], properties => { errors => { type => 'array', items => { type => 'object', required => ['message'], properties => {message => {type => 'string'}, path => {type => 'string'}} } } } }; } sub _default_schema_v2 { my ($self, $schema) = @_; +{description => 'Default response.', schema => $schema}; } sub _default_schema_v3 { my ($self, $schema) = @_; +{ description => 'default Mojolicious::Plugin::OpenAPI response', content => {'application/json' => {schema => $schema}}, }; } 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_validate { my ($c, $args) = @_; # code() can be set by other methods such as $c->openapi->cors_simple() return [{message => 'Already rendered.'}] if $c->res->code; # Write validated data to $c->validation->output my $self = _self($c); my $op_spec = $c->openapi->spec; local $op_spec->{parameters} = $self->_parameters_for($c->req->method, $c->stash('openapi.path'),); my @errors = $self->validator->validate_request($c, $op_spec, $c->validation->output); if (@errors) { $self->_log($c, '<<<', \@errors); $c->stash(status => 400) ->render(data => $self->_renderer->($c, {errors => \@errors, status => 400})) if $args->{auto_render} // 1; } 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 _parameters_for { $_[0]->{parameters_for}{$_[2]}{lc($_[1])} || [] } sub _render { my ($renderer, $c, $output, $args) = @_; return unless exists $c->stash->{openapi}; return unless my $self = _self($c); my $res = $c->stash('openapi'); my $status = $args->{status} ||= ($c->stash('status') || 200); my $op_spec = $c->openapi->spec || {responses => {$status => {schema => $self->_default_schema}}}; my @errors; delete $args->{encoding}; $c->stash->{format} ||= 'json'; if ($op_spec->{responses}{$status} or $op_spec->{responses}{default}) { @errors = $self->validator->validate_response($c, $op_spec, $status, $res); $args->{status} = 500 if @errors; } else { $args->{status} = 501; @errors = ({message => qq(No response rule for "$status".)}); } $self->_log($c, '>>>', \@errors) if @errors; $c->stash(status => $args->{status}); $$output = $self->_renderer->($c, @errors ? {errors => \@errors, status => $status} : $res); } sub _openapi_path_to_route_path { my ($self, $http_method, $openapi_path) = @_; my %params = map { ($_->{name}, $_) } @{$self->_parameters_for($http_method, $openapi_path)}; $openapi_path =~ s/{([^}]+)}/{ my $name = $1; my $type = $params{$name}{'x-mojo-placeholder'} || ':'; "<$type$name>"; }/ge; return $openapi_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 use Mojolicious::Lite; # 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->validation->param("body")}; # 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:///spec.json"}; app->start; __DATA__ @@ spec.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Echo Service" }, "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" } } } } } } } See L for a tutorial on how to write a "full" app with application class and controllers. =head1 DESCRIPTION L is L that add routes and input/output validation to your L application based on a OpenAPI (Swagger) specification. Have a look at the L for references to more documentation, or jump right to the L. Currently v2 is very well supported, while v3 should be considered EXPERIMENTAL. 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. Validated input parameters will be copied to C, which again can be extracted by the "name" in the parameters list from the spec. Example: # specification: "parameters": [{"in": "body", "name": "whatever", "schema": {"type": "object"}}], # controller my $body = $c->validation->param("whatever"); =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 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 allow_invalid_ref The OpenAPI specification does not allow "$ref" at every level, but setting this flag to a true value will ignore the $ref check. Note that setting this attribute is discourage. =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_codes A list of response codes that will get a C<"$ref"> pointing to "#/definitions/DefaultResponse", unless already defined in the spec. "DefaultResponse" can be altered by setting L. The default response code list is the following: 400 | Bad Request | Invalid input from client / user agent 401 | Unauthorized | Used by Mojolicious::Plugin::OpenAPI::Security 404 | Not Found | Route is not defined 500 | Internal Server Error | Internal error or failed output validation 501 | Not Implemented | Route exists, but the action is not implemented Note that more default codes might be added in the future if required by the plugin. =head3 default_response_name The name of the "definition" in the spec that will be used for L. The default value is "DefaultResponse". See L for more details. =head3 log_level C is used when logging invalid request/response error messages. Default: "warn". =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 schema Can be used to set a different schema, than the default OpenAPI 2.0 spec. Example values: "http://swagger.io/v2/schema.json", "v2" or "v3". =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 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. =head3 version_from_class Can be used to overridden C in the API specification, from the return value from the C method in C. This will only have an effect if "version" is "0". Defaults to the current C<$app>. =head1 AUTHORS Henrik Andersen Ilya Rassadin Jan Henning Thorsen Joel Berger =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 =item * L =item * L =item * L =item * L =back =cut Mojolicious-Plugin-OpenAPI-2.21/lib/Mojolicious/Plugin/OpenAPI/000755 000765 000024 00000000000 13612462655 025333 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OpenAPI-2.21/lib/Mojolicious/Plugin/OpenAPI/Cors.pm000644 000765 000024 00000033347 13431336325 026602 0ustar00jhthorsenstaff000000 000000 package Mojolicious::Plugin::OpenAPI::Cors; use Mojo::Base -base; 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, $openapi, $config) = @_; 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(@_) }); # TODO: Remove support for openapi.cors_simple $app->helper( 'openapi.cors_simple' => sub { $self->_exchange(shift->stash('openapi.cors_simple_deprecated' => 1), @_); } ); } 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->via}, '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; # TODO: Remove support for openapi.cors_simple if ($c->stash('openapi.cors_simple_deprecated')) { warn "\$c->openapi->cors_simple() has been replaced by \$c->openapi->cors_exchange()"; return _render_bad_request($c, '/Origin') unless $c->res->headers->access_control_allow_origin; return $c; } 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->via}; $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}); =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-2.21/lib/Mojolicious/Plugin/OpenAPI/Security.pm000644 000765 000024 00000015434 13520625612 027477 0ustar00jhthorsenstaff000000 000000 package Mojolicious::Plugin::OpenAPI::Security; use Mojo::Base -base; my %DEF_PATH = (2 => '/securityDefinitions', 3 => '/components/securitySchemes'); sub register { my ($self, $app, $openapi, $config) = @_; my $handlers = $config->{security} or return; return unless $openapi->validator->get($DEF_PATH{$openapi->validator->version}); 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->version}); 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++; } $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-2.21/lib/Mojolicious/Plugin/OpenAPI/Guides/000755 000765 000024 00000000000 13612462655 026553 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OpenAPI-2.21/lib/Mojolicious/Plugin/OpenAPI/SpecRenderer.pm000644 000765 000024 00000053274 13555474551 030271 0ustar00jhthorsenstaff000000 000000 package Mojolicious::Plugin::OpenAPI::SpecRenderer; use Mojo::Base -base; use Mojo::JSON; use constant DEBUG => $ENV{MOJO_OPENAPI_DEBUG} || 0; use constant MARKDOWN => eval 'require Text::Markdown;1'; sub register { my ($self, $app, $openapi, $config) = @_; if ($config->{render_specification} // 1) { my $spec_route = $openapi->route->get('/')->to(cb => sub { shift->openapi->render_spec }); my $name = $config->{spec_route_name} || $openapi->validator->get('/x-mojo-name'); $spec_route->name($name) if $name; push @{$app->renderer->classes}, __PACKAGE__ unless $app->{'openapi.render_specification'}++; } if ($config->{render_specification_for_paths} // 1) { $app->plugins->once(openapi_routes_added => sub { $self->_add_documentation_routes(@_) }); } $app->helper('openapi.render_spec' => \&_render_spec); } 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 { _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 _markdown { return Mojo::ByteStream->new(MARKDOWN ? Text::Markdown::markdown($_[0]) : $_[0]); } sub _render_partial_spec { my ($c, $path) = @_; my $self = Mojolicious::Plugin::OpenAPI::_self($c); my $method = $c->param('method'); my $bundled = $self->validator->get([paths => $path]); $bundled = $self->validator->bundle({schema => $bundled}) if $bundled; my $definitions = $bundled->{definitions} || {} if $bundled; my $parameters = $bundled->{parameters} || []; if ($method and $bundled = $bundled->{$method}) { push @$parameters, @{$bundled->{parameters} || []}; } return $c->render(json => {errors => [{message => 'No spec defined.'}]}, status => 404) unless $bundled; delete $bundled->{$_} for qw(definitions parameters); return $c->render( json => { '$schema' => 'http://json-schema.org/draft-04/schema#', title => $self->validator->get([qw(info title)]) || '', description => $self->validator->get([qw(info description)]) || '', definitions => $definitions, parameters => $parameters, %$bundled, } ); } sub _render_spec { return _render_partial_spec(@_) if $_[1]; my $c = shift; my $self = Mojolicious::Plugin::OpenAPI::_self($c); my $format = $c->stash('format') || 'json'; my $spec = $self->{bundled} ||= $self->validator->bundle; local $spec->{basePath} = $spec->{basePath}; local $spec->{host} = $spec->{host}; local $spec->{servers} = $spec->{servers}; if ($self->validator->version ge '3') { $spec->{servers} = [{url => $c->req->url->to_abs->to_string}]; delete @$spec{qw(basePath host)}; } else { $spec->{basePath} = $c->url_for($spec->{basePath}); $spec->{host} = $c->req->url->to_abs->host_port; delete $spec->{servers}; } return $c->render(json => $spec) unless $format eq 'html'; return $c->render( handler => 'ep', template => 'mojolicious/plugin/openapi/layout', esc => sub { local $_ = shift; s/\W/-/g; $_ }, markdown => \&_markdown, serialize => \&_serialize, spec => $spec, X_RE => qr{^x-}, ); } sub _serialize { Mojo::JSON::encode_json(@_) } 1; =encoding utf8 =head1 NAME Mojolicious::Plugin::OpenAPI::SpecRenderer - Render OpenAPI specification =head1 SYNOPSIS $app->plugin(OpenAPI => { plugins => [qw(+SpecRenderer)], render_specification => 1, render_specification_for_paths => 1, }); =head1 DESCRIPTION L will enable L to render the specification in both HTML and JSON format. 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. =head1 HELPERS =head2 openapi.render_spec $c = $c->openapi->render_spec; $c = $c->openapi->render_spec($openapi_path); $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. =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 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} %>

%= include "mojolicious/plugin/openapi/toc" % if ($spec->{info}{description}) {

Description

%== $markdown->($spec->{info}{description})
% } % if ($spec->{info}{termsOfService}) {

Terms of service

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

% } @@ mojolicious/plugin/openapi/footer.html.ep % 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} %>

% } @@ mojolicious/plugin/openapi/human.html.ep % if ($spec->{summary}) {

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

% } % if ($spec->{description}) {
<%== $markdown->($spec->{description}) %>
% } % if (!$spec->{description} and !$spec->{summary}) {

This resource is not documented.

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

Parameters

% if ($has_parameters) { % } % for my $p (@{$op->{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} ? $markdown->($p->{description}) : "" %>
% } else {

This resource has no input parameters.

% } % if ($body) {

Body

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

requestBody

<%= $serialize->($op->{requestBody}{content}) %>
% } @@ mojolicious/plugin/openapi/response.html.ep % for my $code (sort keys %{$op->{responses}}) { % next if $code =~ $X_RE; % my $res = $op->{responses}{$code};

Response <%= $code %>

%= include "mojolicious/plugin/openapi/human", spec => $res
<%= $serialize->($res->{schema} || $res->{content}) %>
% } @@ mojolicious/plugin/openapi/resource.html.ep

"><%= uc $method %> <%= $spec->{basePath} %><%= $path %>

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

This resource is deprecated!

% } % if ($op->{operationId}) {

Operation ID: <%= $op->{operationId} %>

% } %= include "mojolicious/plugin/openapi/human", spec => $op %= include "mojolicious/plugin/openapi/parameters", op => $op %= include "mojolicious/plugin/openapi/response", op => $op @@ mojolicious/plugin/openapi/references.html.ep % use Mojo::ByteStream 'b';

References

% for my $key (sort { $a cmp $b } keys %{$spec->{definitions} || {}}) { % next if $key =~ $X_RE;

#/definitions/<%= $key %>

<%= $serialize->($spec->{definitions}{$key}) %>
% } % for my $type (sort { $a cmp $b } keys %{$spec->{components} || {}}) { % for my $key (sort { $a cmp $b } keys %{$spec->{components}{$type} || {}}) { % next if $key =~ $X_RE;

#/components/<%= $type %>/<%= $key %>

<%= $serialize->($spec->{components}{$type}{$key}) %>
% } % } % for my $key (sort { $a cmp $b } keys %{$spec->{parameters} || {}}) { % next if $key =~ $X_RE; % my $item = $spec->{parameters}{$key};

#/parameters/<%= $key %> - "<%= $item->{name} %>"

<%= $item->{description} || 'No description.' %>

  • In: <%= $item->{in} %>
  • Type: <%= $item->{type} %><%= $item->{format} ? " / $item->{format}" : "" %><%= $item->{pattern} ? " / $item->{pattern}" : ""%>
  • % if ($item->{exclusiveMinimum} || $item->{exclusiveMaximum} || $item->{minimum} || $item->{maximum}) {
  • Min / max: <%= $item->{exclusiveMinimum} ? "$item->{exclusiveMinimum} <" : $item->{minimum} ? "$item->{minimum} <=" : b("∞ <=") %> value <%= $item->{exclusiveMaximum} ? "< $item->{exclusiveMaximum}" : $item->{maximum} ? "<= $item->{maximum}" : b("<= ∞") %>
  • % } % if ($item->{minLength} || $item->{maxLength}) {
  • Min / max: <%= $item->{minLength} ? "$item->{minLength} <=" : b("∞ <=") %> length <%= $item->{maxLength} ? "<= $item->{maxLength}" : b("<= ∞") %>
  • % } % if ($item->{minItems} || $item->{maxItems}) {
  • Min / max: <%= $item->{minItems} ? "$item->{minItems} <=" : b("∞ <=") %> items <%= $item->{maxItems} ? "<= $item->{maxItems}" : b("<= ∞") %>
  • % } % for my $k (qw(collectionFormat uniqueItems multipleOf enum)) { % next unless $item->{$k};
  • <%= ucfirst $k %>: <%= ref $item->{$k} ? $serialize->($item->{$k}) : $item->{$k} %>
  • % }
  • Required: <%= $item->{required} ? 'Yes.' : 'No.' %>
  • <%= defined $item->{default} ? "Default: " . $serialize->($item->{default}) : 'No default value.' %>
% for my $k (qw(items schema)) { % next unless $item->{$k};
<%= $serialize->($item->{$k}) %>
% } % } @@ mojolicious/plugin/openapi/resources.html.ep

Resources

% 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 %>
  • % }
% } % for my $path (sort { length $a <=> length $b } keys %{$spec->{paths}}) { % next if $path =~ $X_RE; % for my $http_method (sort keys %{$spec->{paths}{$path}}) { % next if $http_method =~ $X_RE or $http_method eq 'parameters'; % my $op = $spec->{paths}{$path}{$http_method}; %= include "mojolicious/plugin/openapi/resource", method => $http_method, op => $op, path => $path % } % } @@ mojolicious/plugin/openapi/toc.html.ep @@ mojolicious/plugin/openapi/layout.html.ep <%= $spec->{info}{title} || 'No title' %>
%= include "mojolicious/plugin/openapi/header" %= include "mojolicious/plugin/openapi/resources" %= include "mojolicious/plugin/openapi/references" %= include "mojolicious/plugin/openapi/footer"
Mojolicious-Plugin-OpenAPI-2.21/lib/Mojolicious/Plugin/OpenAPI/Guides/Swagger2.pod000644 000765 000024 00000003263 13372726265 030747 0ustar00jhthorsenstaff000000 000000 =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) 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-2.21/lib/Mojolicious/Plugin/OpenAPI/Guides/Tutorial.pod000644 000765 000024 00000020645 13520400271 031051 0ustar00jhthorsenstaff000000 000000 =head1 NAME Mojolicious::Plugin::OpenAPI::Guides::Tutorial - Mojolicious <3 Open API (Swagger) =head1 OVERVIEW This guide will give you an introduction to how to use L. =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}] $route->to("pet#list", {foo => 123}); =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); } # $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); } 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-2.21/lib/Mojolicious/Plugin/OpenAPI/Guides/OpenAPI3.pod000644 000765 000024 00000024663 13571755615 030615 0ustar00jhthorsenstaff000000 000000 =head1 NAME Mojolicious::Plugin::OpenAPI::Guides::OpenAPI3 - Mojolicious <3 Open API v3 (Swagger) =head1 OVERVIEW This guide will give you an introduction to how to use L with OpenAPI version v3.x =head1 TUTORIAL =head2 Specification This plugin reads an L and generate 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 validtion rules, which is used by L and when rendering the response back to the client, using C<< render(openapi => ...) >>. Here OpenAPIv3 input differs from 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 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}] $route->to("pet#list", {foo => 123}); =item * security and securitySchemes The securityScheme is added under components, where on 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 security callback when loading the plugin $self->plugin( OpenAPI => { spec => $self->static->file("openapi.json")->path, schema => 'v3', 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"), schema => 'v3'}); } 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); } # $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); } 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: { "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-2.21/lib/JSON/Validator/000755 000765 000024 00000000000 13612462655 023044 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OpenAPI-2.21/lib/JSON/Validator/OpenAPI/000755 000765 000024 00000000000 13612462655 024277 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OpenAPI-2.21/lib/JSON/Validator/OpenAPI/Mojolicious.pm000644 000765 000024 00000047167 13612462550 027142 0ustar00jhthorsenstaff000000 000000 package JSON::Validator::OpenAPI::Mojolicious; use Mojo::Base 'JSON::Validator'; use Carp 'confess'; use Mojo::Util; use Scalar::Util 'looks_like_number'; use Time::Local (); use constant DEBUG => $ENV{JSON_VALIDATOR_DEBUG} || 0; use constant IV_SIZE => eval 'require Config;$Config::Config{ivsize}'; our %COLLECTION_RE = (pipes => qr{\|}, csv => qr{,}, ssv => qr{\s}, tsv => qr{\t}); our %VERSIONS = ( v2 => 'http://swagger.io/v2/schema.json', v3 => 'https://spec.openapis.org/oas/3.0/schema/2019-04-02' ); has version => 2; sub E { JSON::Validator::Error->new(@_) } has generate_definitions_path => sub { my $self = shift; Scalar::Util::weaken($self); return sub { my $ref = shift; if ($self->version eq '3') { # Try to determine the path from the fqn # We are only interested in the path in the fqn, so following fqn: # # #/components/schemas/some_schema, the returned path with be ['components', 'schemas'] my $path = Mojo::Path->new($ref->fqn =~ m!^.*#/(components/.+)$!)->to_dir->parts; return $path->[0] ? $path : ['definitions']; } else { # By default return definitions as path return ['definitions']; } }; }; sub load_and_validate_schema { my ($self, $spec, $args) = @_; $spec = $self->bundle({replace => 1, schema => $spec}) if $args->{allow_invalid_ref}; local $args->{schema} = $args->{schema} ? $VERSIONS{$args->{schema}} || $args->{schema} : $VERSIONS{v2}; $self->version($1) if !$self->{version} and $args->{schema} =~ m!(?:/v||/oas/)(\d)!; my @errors; my $gather = sub { push @errors, E($_[1], 'Only one parameter can have "in":"body"') if 1 < grep { $_->{in} eq 'body' } @{$_[0] || []}; }; $self->_get($self->_resolve($spec), ['paths', undef, undef, 'parameters'], '', $gather); confess join "\n", "Invalid JSON specification $spec:", map {"- $_"} @errors if @errors; $self->SUPER::load_and_validate_schema($spec, $args); if (my $class = $args->{version_from_class}) { if (UNIVERSAL::can($class, 'VERSION') and $class->VERSION) { $self->schema->data->{info}{version} ||= $class->VERSION; } } return $self; } sub validate_input { my $self = shift; local $self->{validate_input} = 1; local $self->{root} = $self->schema; $self->validate(@_); } sub validate_request { my ($self, $c, $schema, $input) = @_; my @errors; local $self->{cache}; # v3 Content-Type if (my $body_schema = $schema->{requestBody}) { push @errors, $self->_validate_request_body($c, $body_schema); } for my $p (@{$schema->{parameters} || []}) { my ($in, $name, $type) = @$p{qw(in name type)}; $type ||= $p->{schema}{type} if $p->{schema}; # v3 my ($exists, $value) = (0, undef); if ($in eq 'body') { $value = $self->_get_request_data($c, $in); $exists = length $value if defined $value; } elsif ($in eq 'formData' and $type eq 'file') { $value = $self->_get_request_uploads($c, $name)->[-1]; $exists = $value ? 1 : 0; } else { my $key = $in eq 'header' ? lc $name : $name; $value = $self->_get_request_data($c, $in); $exists = exists $value->{$key}; $value = $value->{$key}; } if (defined $value and $type eq 'array') { $value = $self->_coerce_by_collection_format($value, $p); } ($exists, $value) = (1, $p->{schema}{default}) if !$exists and $p->{schema} and exists $p->{schema}{default}; ($exists, $value) = (1, $p->{default}) if !$exists and exists $p->{default}; if ($type and defined $value) { if ($type ne 'array' and ref $value eq 'ARRAY') { $value = $value->[-1]; } if (($type eq 'integer' or $type eq 'number') and Scalar::Util::looks_like_number($value)) { $value += 0; } elsif ($type eq 'boolean') { if (!$value or $value =~ /^(?:false)$/) { $value = Mojo::JSON->false; } elsif ($value =~ /^(?:1|true)$/) { $value = Mojo::JSON->true; } } } if (my @e = $self->_validate_request_value($p, $name => $value)) { push @errors, @e; } elsif ($exists) { $input->{$name} = $value; $self->_set_request_data($c, $in, $name => $value) if defined $value; } } return @errors; } sub validate_response { my ($self, $c, $schema, $status, $data) = @_; return JSON::Validator::E('/' => "No responses rules defined for status $status.") unless my $res_schema = $schema->{responses}{$status} || $schema->{responses}{default}; if ($self->version eq '3') { my $accept = $self->_negotiate_accept_header($c, $res_schema); return JSON::Validator::E('/' => "No responses rules defined for Accept $accept.") unless $res_schema = $res_schema->{content}{$accept}; $c->stash(openapi_negotiated_content_type => $accept); } my @errors; push @errors, $self->_validate_response_headers($c, $res_schema->{headers}) if $res_schema->{headers}; if ($res_schema->{'x-json-schema'}) { warn "[OpenAPI] Validate using x-json-schema\n" if DEBUG; push @errors, $self->validate($data, $res_schema->{'x-json-schema'}); } elsif ($res_schema->{schema}) { warn "[OpenAPI] Validate using schema\n" if DEBUG; push @errors, $self->validate($data, $res_schema->{schema}); } return @errors; } sub _build_formats { my $self = shift; my $formats = $self->SUPER::_build_formats; $formats->{byte} = \&_match_byte_string; $formats->{double} = sub { _match_number(double => $_[0], '') }; $formats->{float} = sub { _match_number(float => $_[0], '') }; $formats->{int32} = sub { _match_number(int32 => $_[0], 'l') }; $formats->{int64} = sub { _match_number(int64 => $_[0], IV_SIZE >= 8 ? 'q' : '') }; $formats->{password} = sub {undef}; return $formats; } sub _coerce_by_collection_format { my ($self, $data, $p) = @_; my $schema = $p->{schema} || $p; my $type = ($schema->{items} ? $schema->{items}{type} : $schema->{type}) || ''; my $collection_format = $p->{collectionFormat}; my $custom_re; # support for v3 style / explode if ($p->{style}) { if ($p->{style} eq 'simple') { $collection_format = 'csv'; } elsif ($p->{style} eq 'label') { $custom_re = qr{\.}; $collection_format = $p->{explode} ? 'custom' : 'csv' if $data =~ s/^$custom_re//; } elsif ($p->{style} eq 'matrix') { $custom_re = qr{;\Q$p->{name}\E=}; $collection_format = $p->{explode} ? 'custom' : 'csv' if $data =~ s/^$custom_re//; } elsif ($p->{style} eq 'form') { $collection_format = $p->{explode} ? 'multi' : 'csv'; } elsif ($p->{style} eq 'spaceDelimited') { $collection_format = $p->{explode} ? 'multi' : 'ssv'; } elsif ($p->{style} eq 'pipeDelimited') { $collection_format = $p->{explode} ? 'multi' : 'pipes'; } } return $data unless $collection_format; if ($collection_format eq 'multi') { $data = [$data] unless ref $data eq 'ARRAY'; @$data = map { $_ + 0 } @$data if $type eq 'integer' or $type eq 'number'; return $data; } my $re = $collection_format eq 'custom' ? $custom_re : $COLLECTION_RE{$collection_format} || ','; my $single = ref $data eq 'ARRAY' ? 0 : ($data = [$data]); for my $i (0 .. @$data - 1) { my @d = split /$re/, ($data->[$i] // ''); $data->[$i] = ($type eq 'integer' or $type eq 'number') ? [map { $_ + 0 } @d] : \@d; } return $single ? $data->[0] : $data; } sub _confess_invalid_in { confess "Unsupported \$in: $_[0]. Please report at https://github.com/jhthorsen/json-validator"; } sub _get_request_data { my ($self, $c, $in) = @_; if ($in eq 'query') { return $self->{cache}{$in} ||= $c->req->url->query->to_hash(1); } elsif ($in eq 'path') { return $c->match->stack->[-1]; } elsif ($in eq 'formData') { return $self->{cache}{$in} ||= $c->req->body_params->to_hash(1); } elsif ($in eq 'cookie') { return $self->{cache}{$in} ||= {map { ($_->name, $_->value) } @{$c->req->cookies}}; } elsif ($in eq 'header') { my $headers = $c->req->headers->to_hash(1); return $self->{cache}{$in} ||= {map { lc($_) => $headers->{$_} } keys %$headers}; } elsif ($in eq 'body') { return $c->req->json; } else { _confess_invalid_in($in); } } sub _get_request_uploads { my ($self, $c, $name) = @_; return $c->req->every_upload($name); } sub _get_response_data { my ($self, $c, $in) = @_; return $c->res->headers->to_hash(1) if $in eq 'header'; _confess_invalid_in($in); } sub _match_byte_string { $_[0] =~ /^[A-Za-z0-9\+\/\=]+$/ ? undef : 'Does not match byte format.' } sub _match_number { my ($name, $val, $format) = @_; return 'Does not look like an integer' if $name =~ m!^int! and $val !~ /^-?\d+(\.\d+)?$/; return 'Does not look like a number.' unless looks_like_number $val; return undef unless $format; return undef if $val eq unpack $format, pack $format, $val; return "Does not match $name format."; } sub _negotiate_accept_header { my ($self, $c, $schema) = @_; my $accept = $c->req->headers->accept || '*/*'; my @in_schema = sort { length $b <=> length $a } keys %{$schema->{content}}; my (@from_req, %from_req); /^\s*([^,; ]+)(?:\s*\;\s*q\s*=\s*(\d+(?:\.\d+)?))?\s*$/i and $from_req{lc $1} = $2 // 1 for split /,/, $accept; @from_req = sort { $from_req{$b} <=> $from_req{$a} } sort keys %from_req; # Check for exact match for my $ct (@from_req) { return $ct if $schema->{content}{$ct}; } # Check for closest match for my $re (map { s!\*!.*!g; qr{$_} } grep {/\*/} @in_schema) { for my $ct (@from_req) { return $ct if $ct =~ $re; } } for my $re (map { s!\*!.*!g; qr{$_} } grep {/\*/} @from_req) { for my $ct (@in_schema) { return $ct if $ct =~ $re; } } # Could not find any valid content type return $accept; } sub _resolve_ref { my ($self, $topic, $url) = @_; $topic->{'$ref'} = "#/definitions/$topic->{'$ref'}" if $topic->{'$ref'} =~ /^\w+$/; return $self->SUPER::_resolve_ref($topic, $url); } sub _set_request_data { my ($self, $c, $in, $name => $value) = @_; if ($in eq 'query') { $c->req->url->query->merge($name => $value); $c->req->params->merge($name => $value); } elsif ($in eq 'path') { $c->stash($name => $value); } elsif ($in eq 'formData') { $c->req->params->merge($name => $value); $c->req->body_params->merge($name => $value); } elsif ($in eq 'cookie') { $c->req->cookie($name => $value); } elsif ($in eq 'header') { $c->req->headers->header($name => ref $value eq 'ARRAY' ? @$value : $value); } elsif ($in ne 'body') { # no need to write body back _confess_invalid_in($in); } } sub _to_list { return ref $_[0] eq 'ARRAY' ? @{$_[0]} : $_[0] ? ($_[0]) : (); } sub _validate_request_body { my ($self, $c, $body_schema) = @_; my $ct = $c->req->headers->content_type // ''; $ct =~ s!;.*$!!; if (my $schema = $body_schema->{content}{$ct}) { my $body = $self->_get_request_data($c, $ct =~ /\bform\b/ ? 'formData' : 'body'); local $schema->{required} //= $body_schema->{required}; return $self->_validate_request_value($schema, body => $body); } return JSON::Validator::E('/' => "No requestBody rules defined for Content-Type $ct.") if $ct; return JSON::Validator::E('/', 'Invalid Content-Type.'); } sub _validate_request_value { my ($self, $p, $name, $value) = @_; my $type = $p->{type} || 'object'; my @e; return if !defined $value and !$p->{required}; my $in = $p->{in} // 'body'; my $schema = { properties => {$name => $p->{'x-json-schema'} || $p->{schema} || $p}, required => [$p->{required} ? ($name) : ()] }; if ($in eq 'body') { warn "[OpenAPI] Validate $in $name\n" if DEBUG; if ($p->{'x-json-schema'}) { return $self->validate({$name => $value}, $schema); } else { return $self->validate_input({$name => $value}, $schema); } } elsif (defined $value) { warn "[OpenAPI] Validate $in $name=$value\n" if DEBUG; return $self->validate_input({$name => $value}, $schema); } else { warn "[OpenAPI] Validate $in $name=undef\n" if DEBUG; return $self->validate_input({$name => $value}, $schema); } return; } sub _validate_response_headers { my ($self, $c, $schema) = @_; my $input = $self->_get_response_data($c, 'header'); my $version = $self->version; my @errors; for my $name (keys %$schema) { my $p = $schema->{$name}; $p = $p->{schema} if $version eq '3'; # jhthorsen: I think that the only way to make a header required, # is by defining "array" and "minItems" >= 1. if ($p->{type} eq 'array') { push @errors, $self->validate($input->{$name}, $p); } elsif ($input->{$name}) { push @errors, $self->validate($input->{$name}[0], $p); $c->res->headers->header($name => $input->{$name}[0] ? 'true' : 'false') if $p->{type} eq 'boolean' and !@errors; } } return @errors; } sub _validate_type_array { my ($self, $data, $path, $schema) = @_; if (ref $schema->{items} eq 'HASH' and ($schema->{items}{type} || '') eq 'array') { $data = $self->_coerce_by_collection_format($data, $schema->{items}); } return $self->SUPER::_validate_type_array($data, $path, $schema); } sub _validate_type_file { my ($self, $data, $path, $schema) = @_; return unless $schema->{required} and (not defined $data or not length $data); return JSON::Validator::E($path => 'Missing property.'); } sub _validate_type_object { my ($self, $data, $path, $schema) = @_; return shift->SUPER::_validate_type_object(@_) unless ref $data eq 'HASH'; # Support "nullable" in v3 # "nullable" is the same as "type":["null", ...], which is supported by many # tools, even though not officially supported by OpenAPI. my %properties = %{$schema->{properties} || {}}; local $schema->{properties} = \%properties; if ($self->version eq '3') { for my $key (keys %properties) { next unless $properties{$key}{nullable}; $properties{$key} = {%{$properties{$key}}}; $properties{$key}{type} = ['null', _to_list($properties{$key}{type})]; } } return shift->SUPER::_validate_type_object(@_) unless $self->{validate_input}; my (@e, %ro); for my $p (keys %properties) { next unless $properties{$p}{readOnly}; push @e, JSON::Validator::E("$path/$p", "Read-only.") if exists $data->{$p}; $ro{$p} = 1; } my $discriminator = $schema->{discriminator}; if ($discriminator and !$self->{inside_discriminator}) { my $name = $data->{$discriminator} or return JSON::Validator::E($path, "Discriminator $discriminator has no value."); my $dschema = $self->{root}->get("/definitions/$name") or return JSON::Validator::E($path, "No definition for discriminator $name."); local $self->{inside_discriminator} = 1; # prevent recursion return $self->_validate($data, $path, $dschema); } local $schema->{required} = [grep { !$ro{$_} } @{$schema->{required} || []}]; return @e, $self->SUPER::_validate_type_object($data, $path, $schema); } 1; =encoding utf8 =head1 NAME JSON::Validator::OpenAPI::Mojolicious - JSON::Validator request/response adapter for Mojolicious =head1 SYNOPSIS my $validator = JSON::Validator::OpenAPI::Mojolicious->new; $validator->load_and_validate_schema("myschema.json"); my @errors = $validator->validate_request( $c, $validator->get([paths => "/wharever", "get"]), $c->validation->output, ); @errors = $validator->validate_response( $c, $validator->get([paths => "/wharever", "get"]), 200, {some => {response => "data"}}, ); =head1 DESCRIPTION L is a module for validating request and response data from/to your L application. Do not use this module directly. Use L instead. =head1 STASH VARIABLES =head2 openapi_negotiated_content_type $str = %c->stash("openapi_negotiated_content_type"); This value will be set when the Accept header has been validated successfully against an OpenAPI v3 schema. Note that this could have the value of "*/*" or other invalid "Content-Header" values. It will be C if the "Accept" header is not accepteed. Unfortunately, this variable is not set until you call L, since we need a status code to figure out which types are accepted. This means that if you want to validate the "Accept" header on input, then you have to specify that as a parameter in the spec. =head1 ATTRIBUTES L inherits all attributes from L. =head2 formats $validator = $validator->formats({}); $hash_ref = $validator->formats; Open API support the same formats as L, but adds the following to the set: =over 4 =item * byte A padded, base64-encoded string of bytes, encoded with a URL and filename safe alphabet. Defined by RFC4648. =item * date An RFC3339 date in the format YYYY-MM-DD =item * double Cannot test double values with higher precision then what the "number" type already provides. =item * float Will always be true if the input is a number, meaning there is no difference between L and L. Patches are welcome. =item * int32 A signed 32 bit integer. =item * int64 A signed 64 bit integer. Note: This check is only available if Perl is compiled to use 64 bit integers. =back =head2 version $str = $validator->version; Used to get the OpenAPI Schema version to use. Will be set automatically when using L, unless already set. Supported values are "2" an "3". =head1 METHODS L inherits all attributes from L. =head2 load_and_validate_schema $validator = $validator->load_and_validate_schema($schema, \%args); Will load and validate C<$schema> against the OpenAPI specification. C<$schema> can be anything L accepts. The expanded specification will be stored in L on success. See L for the different version of C<$url> that can be accepted. C<%args> can be used to further instruct the expansion and validation process: =over 2 =item * allow_invalid_ref Setting this to a true value, will disable the first pass. This is useful if you don't like the restrictions set by OpenAPI, regarding where you can use C<$ref> in your specification. =item * version_from_class Setting this to a module/class name will use the version number from the class and overwrite the version in the specification: { "info": { "version": "1.00" // <-- this value } } =back The validation is done with a two pass process: =over 2 =item 1. First it will check if the C<$ref> is only specified on the correct places. This can be disabled by setting L to a true value. =item 2. Validate the expanded version of the spec, (without any C<$ref>) against the OpenAPI schema. =back =head2 generate_definitions_path See L. =head2 validate_input @errors = $validator->validate_input($data, $schema); This method will make sure "readOnly" is taken into account, when validating data sent to your API. =head2 validate_request @errors = $validator->validate_request($c, $schema, \%input); Takes an L and a schema definition and returns a list of errors, if any. Validated input parameters are moved into the C<%input> hash. =head2 validate_response @errors = $validator->validate_response($c, $schema, $status, $data); =head1 SEE ALSO L. L. L =cut Mojolicious-Plugin-OpenAPI-2.21/t/v3-nullable.t000644 000765 000024 00000002711 13464315503 022347 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; my %data = (id => 42); get '/nullable-data' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => \%data); }, 'withNullable'; plugin OpenAPI => {url => 'data:///nullable.json', schema => 'v3'}; 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); done_testing; __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" } } } } } } } }, "components": { "schemas": { "WithNullable": { "required": [ "id", "name" ], "properties": { "id": { "type": "integer", "format": "int64" }, "name": { "type": "string", "nullable": true } } } } } } Mojolicious-Plugin-OpenAPI-2.21/t/swagger2.t000644 000765 000024 00000002466 13372726265 021764 0ustar00jhthorsenstaff000000 000000 use 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-2.21/t/correct-order-of-paths.t000644 000765 000024 00000002346 13422001552 024506 0ustar00jhthorsenstaff000000 000000 use 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-2.21/t/cors.t000644 000765 000024 00000015122 13422743647 021201 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::Mojo; use Test::More; our $cors_callback = 'main::cors_exchange'; our $cors_method = 'cors_exchange'; use Mojolicious::Lite; get '/user' => sub { my $c = shift->openapi->$cors_method($cors_callback)->openapi->valid_input or return; $c->render(json => {cors => $cors_method, 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; for (qw(cors_simple cors_exchange)) { note 'Simple'; local $cors_method = $_; $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_method)->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-2.21/t/load-from-app.t000644 000765 000024 00000001740 13403122304 022646 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use JSON::Validator::OpenAPI::Mojolicious; use Mojolicious; use Test::More; my $validator = JSON::Validator::OpenAPI::Mojolicious->new; $validator->ua->server->app(Mojolicious->new); $validator->ua->server->app->routes->get( '/api' => sub { my $c = shift; $c->render( json => { swagger => $c->param('fail') ? undef : '2.0', info => {version => '0.8', title => 'Test client spec'}, schemes => ['http'], host => 'api.example.com', basePath => '/v1', paths => {}, } ); } ); eval { $validator->load_and_validate_schema('/api') }; # Some CPAN testers says: [JSON::Validator] GET http://127.0.0.1:61594/api == Service Unavailable at JSON/Validator.pm line 274. plan skip_all => $@ if $@ =~ /\sGET\s/i; is $@, '', 'loaded valid schema from app'; eval { $validator->load_and_validate_schema('/api?fail=1') }; like $@, qr{got null}, 'loaded invalido schema from app'; done_testing; Mojolicious-Plugin-OpenAPI-2.21/t/default-value.t000644 000765 000024 00000005516 13414512733 022766 0ustar00jhthorsenstaff000000 000000 use 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/echo.json"}); } #============================================================================ package MyApp::Controller::Dummy; use Mojo::Base 'Mojolicious::Controller'; sub echo { 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')}, name => $name, x_foo => {header => $c->req->headers->header('X-Foo')}, validation => $c->validation->output, } ); } #============================================================================ package main; my $t = Test::Mojo->new('MyApp'); $t->get_ok('/api/echo/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'; $t->post_ok('/api/echo')->status_is(200)->json_is('/days' => {controller => 42, url => 42}) ->json_is('/name', {controller => 'batman', form => 'batman'}) ->json_is('/x_foo', {header => 'yikes'}) ->json_is('/validation', {days => 42, name => 'batman', 'X-Foo' => 'yikes', enumParam => '10.1.0'}); done_testing; __DATA__ @@ echo.json { "swagger": "2.0", "info": { "version": "0.8", "title": "Pets" }, "schemes": [ "http" ], "basePath": "/api", "paths": { "/echo": { "post": { "x-mojo-to": "dummy#echo", "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/{name}": { "get": { "x-mojo-to": "dummy#echo", "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" } } } } } } } Mojolicious-Plugin-OpenAPI-2.21/t/bundle.t000644 000765 000024 00000001316 13403122167 021467 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Mojo::File 'path'; use Test::More; use JSON::Validator::OpenAPI::Mojolicious; # 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::OpenAPI::Mojolicious->new; my $bundlecheck_path = path(path(__FILE__)->dirname, 'spec', File::Spec->updir, 'spec', 'bundlecheck.json'); my $bundled = $validator->schema($bundlecheck_path)->bundle; eval { $validator->load_and_validate_schema($bundled) }; is $@, '', 'bundled schema is valid'; done_testing; Mojolicious-Plugin-OpenAPI-2.21/t/empty-string.t000644 000765 000024 00000002156 13351647463 022700 0ustar00jhthorsenstaff000000 000000 use 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-2.21/t/reply-spec.t000644 000765 000024 00000005474 13372726265 022330 0ustar00jhthorsenstaff000000 000000 use 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', 'POST /api/pets'); $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', 'POST /api/pets'); 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')->to(cb => sub { shift->openapi->render_spec })->name('docs'); $app->routes->post('/pets')->to(cb => 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": { "post": { "operationId": "addPet", "parameters": [ { "in": "body", "name": "body", "schema": { "type": "object" } } ], "responses": { "200": { "description": "pet response", "schema": { "type": "object" } } } } } } } Mojolicious-Plugin-OpenAPI-2.21/t/renderer.t000644 000765 000024 00000004327 13520625612 022034 0ustar00jhthorsenstaff000000 000000 use 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', '/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" } } } } } } } Mojolicious-Plugin-OpenAPI-2.21/t/example-array-of-hashes.t000644 000765 000024 00000002002 13372726265 024647 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; post '/echo' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->req->json); }, 'echo'; plugin OpenAPI => {url => 'data://main/echo.json'}; my $t = Test::Mojo->new; $t->post_ok('/api/echo' => json => [{foo => 'f'}, {bar => 'b'}])->status_is(200) ->json_is('/0' => {foo => 'f'})->json_is('/1' => {bar => 'b'}); done_testing; __DATA__ @@ echo.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Array" }, "basePath" : "/api", "paths" : { "/echo" : { "post" : { "x-mojo-name" : "echo", "parameters" : [ {"in": "body", "name": "body", "schema": {"$ref": "#/definitions/s1"}} ], "responses" : { "200": { "description": "Echo response", "schema": {"$ref": "#/definitions/s1"} } } } } }, "definitions": { "s1": {"type" : "array", "items": {"type": "object"}} } } Mojolicious-Plugin-OpenAPI-2.21/t/v3-invalid_file_refs.t000644 000765 000024 00000001550 13571755615 024230 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; use JSON::Validator::OpenAPI::Mojolicious; get '/test' => sub { my $c = shift->openapi->valid_input or return; $c->render(status => 200, openapi => $c->param('pcversion')); }, 'File'; plugin OpenAPI => {schema => 'v3', 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('/definitions') ->content_like(qr/v3-invalid_include_yaml-PCVersion-/); my $json = $t->get_ok('/api')->tx->res->body; my $validator = JSON::Validator::OpenAPI::Mojolicious->new(version => 3); eval { $validator->load_and_validate_schema($json, {schema => 'v3'}) }; like $@, qr/Properties not allowed: definitions/, 'load_and_validate_schema fails, wrong placement of data'; done_testing; Mojolicious-Plugin-OpenAPI-2.21/t/spec/000755 000765 000024 00000000000 13612462655 020775 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OpenAPI-2.21/t/v3-body.t000644 000765 000024 00000002705 13574520034 021510 0ustar00jhthorsenstaff000000 000000 use 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'; plugin OpenAPI => {url => 'data:///api.yml', schema => 'v3'}; 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 null.'); 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 Mojolicious-Plugin-OpenAPI-2.21/t/v3-security.t000644 000765 000024 00000030473 13574520034 022425 0ustar00jhthorsenstaff000000 000000 use 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', schema => 'v3', 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'}]}); is_deeply \%checks, {fail1 => 1, pass1 => 1}, 'expected checks occurred'; } { local %checks; $t->post_ok('/api/multiple_fail' => json => {})->status_is(401)->json_is( { 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( { 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'}]}); 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 { "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-2.21/t/headers.t000644 000765 000024 00000004654 13372726265 021657 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; my $what_ever; get '/headers' => sub { my $c = shift->openapi->valid_input or return; 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); }, '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) ->json_is('/x-number', 42.3)->header_is('what-ever', '123'); $what_ever = [qw(1 2 3)]; $t->get_ok('/api/headers' => {'x-array' => [42, 24]})->status_is(200) ->json_is('/x-array', [42, 24])->header_is('what-ever', '1, 2, 3'); 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-2.21/t/v3-valid_file_refs.t000644 000765 000024 00000001403 13571755615 023676 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; use JSON::Validator::OpenAPI::Mojolicious; get '/test' => sub { my $c = shift->openapi->valid_input or return; $c->render(status => 200, openapi => $c->param('pcversion')); }, 'File'; plugin OpenAPI => {schema => 'v3', 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 $json = $t->get_ok('/api')->tx->res->body; my $validator = JSON::Validator::OpenAPI::Mojolicious->new(version => 3); is $validator->load_and_validate_schema($json, {schema => 'v3'}), $validator, 'load_and_validate_schema; prove we get a valid spec'; done_testing; Mojolicious-Plugin-OpenAPI-2.21/t/v2-formats.t000644 000765 000024 00000007540 13414512076 022227 0ustar00jhthorsenstaff000000 000000 use lib '.'; use JSON::Validator::OpenAPI::Mojolicious; use Test::More; my $schema = {type => 'object', properties => {v => {type => 'string'}}}; my $validator = JSON::Validator::OpenAPI::Mojolicious->new; sub E { goto &JSON::Validator::OpenAPI::Mojolicious::E; } sub validate_ok { my ($data, $schema, @expected) = @_; my $descr = @expected ? "errors: @expected" : "valid: " . Mojo::JSON::encode_json($data); my @errors = $validator->schema($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::OpenAPI::Mojolicious::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-2.21/t/load-and-validate-spec.t000644 000765 000024 00000005016 13403122304 024406 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::More; use JSON::Validator::OpenAPI::Mojolicious; my $validator = JSON::Validator::OpenAPI::Mojolicious->new; is $validator->load_and_validate_schema('data://main/echo.json'), $validator, 'load_and_validate_schema no options'; is $validator->schema->get('/info/version'), '42.0', 'version'; eval { $validator->load_and_validate_schema('data://main/swagger2/issues/89.json') }; like $@, qr{/definitions/\$ref}si, 'ref in the wrong place'; eval { $validator->load_and_validate_schema('data://main/swagger2/issues/89.json', {allow_invalid_ref => 1, version_from_class => 'JSON::Validator'}); is $validator->schema->get('/info/version'), JSON::Validator->VERSION, 'version_from_class'; is_deeply $validator->schema->get('/definitions/foo/properties'), {}, 'allow_invalid_ref'; } or diag $@; eval { $validator->load_and_validate_schema('data://main/cannot-have-two-bodies.json') }; like $@, qr{Only one parameter can have "in":"body"}si, 'only one parameter can have "in":"body"'; done_testing; __DATA__ @@ echo.json { "swagger" : "2.0", "info" : { "version": "42.0", "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" } }, "400": { "description": "Echo response", "schema": { "type": "object" } } } } } } } @@ swagger2/issues/89.json { "swagger" : "2.0", "info" : { "version": "0", "title" : "Test auto response" }, "paths" : { "$ref": "#/x-def/paths" }, "definitions": { "$ref": "#/x-def/defs" }, "x-def": { "defs": { "foo": { "properties": {} } }, "paths": { "/auto" : { "post" : { "responses" : { "200": { "description": "response", "schema": { "type": "object" } } } } } } } } @@ cannot-have-two-bodies.json { "swagger" : "2.0", "info" : { "version": "42.0", "title" : "Pets" }, "basePath" : "/api", "paths" : { "/echo" : { "post" : { "parameters" : [ { "in": "body", "name": "body", "schema": { "type" : "object" } }, { "in": "body", "name": "body2", "schema": { "type" : "object" } } ], "responses" : { "200": { "description": "Echo response", "schema": { "type": "object" } } } } } } } Mojolicious-Plugin-OpenAPI-2.21/t/x-mojo-placeholder.t000644 000765 000024 00000003417 13451550173 023720 0ustar00jhthorsenstaff000000 000000 use 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-2.21/t/v2-readonly.t000644 000765 000024 00000002525 13402376512 022367 0ustar00jhthorsenstaff000000 000000 use 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 => '/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-2.21/t/00-basic.t000644 000765 000024 00000002134 13013613260 021507 0ustar00jhthorsenstaff000000 000000 use Test::More; use File::Find; if(($ENV{HARNESS_PERL_SWITCHES} || '') =~ /Devel::Cover/) { plan skip_all => 'HARNESS_PERL_SWITCHES =~ /Devel::Cover/'; } if(!eval 'use Test::Pod; 1') { *Test::Pod::pod_file_ok = sub { SKIP: { skip "pod_file_ok(@_) (Test::Pod is required)", 1 } }; } if(!eval 'use Test::Pod::Coverage; 1') { *Test::Pod::Coverage::pod_coverage_ok = sub { SKIP: { skip "pod_coverage_ok(@_) (Test::Pod::Coverage is required)", 1 } }; } if(!eval 'use Test::CPAN::Changes; 1') { *Test::CPAN::Changes::changes_file_ok = sub { SKIP: { skip "changes_ok(@_) (Test::CPAN::Changes is required)", 4 } }; } find( { wanted => sub { /\.pm$/ and push @files, $File::Find::name }, no_chdir => 1 }, -e 'blib' ? 'blib' : 'lib', ); plan tests => @files * 3 + 4; 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::CPAN::Changes::changes_file_ok(); Mojolicious-Plugin-OpenAPI-2.21/t/invalid-json.t000644 000765 000024 00000001614 13425006153 022614 0ustar00jhthorsenstaff000000 000000 use 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)->content_like(qr{got null}); 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-2.21/t/custom-format.t000644 000765 000024 00000002630 13414521654 023024 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; my $what_ever; get '/cf' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'custom_format'; my $oap = plugin OpenAPI => {url => 'data://main/cf.json'}; $oap->validator->formats->{need_to_be_x} = sub { $_[0] eq 'x' ? undef : 'Not x.' }; my $t = Test::Mojo->new; $t->get_ok('/api/cf' => json => {str => 'x'})->status_is(200)->content_like(qr{"str":"x"}); $t->get_ok('/api/cf' => json => {str => 'y'})->status_is(400)->content_like(qr{"errors"}); done_testing; __DATA__ @@ cf.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" : { "/cf" : { "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"], "type": "object", "properties": { "str": { "type": "string", "format": "need_to_be_x" } } } } } Mojolicious-Plugin-OpenAPI-2.21/t/spec.t000644 000765 000024 00000007326 13463331032 021156 0ustar00jhthorsenstaff000000 000000 use 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 { my $jv = JSON::Validator->new(version => undef); is $jv->version, undef, 'no version before load_and_validate_schema'; $jv->load_and_validate_schema($t->tx->res->json); is $jv->version, 4, 'valid schema'; } or do { ok 0, "api/spec did not return a 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-2.21/t/validate.t000644 000765 000024 00000002644 13463320435 022020 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::More; use Mojolicious::Lite; eval { plugin OpenAPI => {url => 'data://main/invalid.json'} }; like $@, qr{Invalid.*Missing}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 => {allow_invalid_ref => 1, url => 'data://main/swagger2/issues/89.json'} }; ok !$@, 'allow_invalid_ref=1' or diag $@; done_testing; __DATA__ @@ invalid.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Test auto response" } } @@ 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-2.21/t/coerce.t000644 000765 000024 00000002117 13372726265 021474 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Mojolicious; use Test::Mojo; use Test::More; my $coerced = t(); $coerced->get_ok('/api/user')->status_is(200)->json_is('/age', 34); my $strict = t(coerce => {}); $strict->get_ok('/api/user')->status_is(500)->json_has('/errors'); sub t { my $t = Test::Mojo->new(Mojolicious->new); $t->app->routes->get( '/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" : { "get" : { "x-mojo-name" : "user", "responses" : { "200": { "description": "User", "schema": { "type": "object", "properties": { "age": { "type": "integer"} } } } } } } } } Mojolicious-Plugin-OpenAPI-2.21/t/issue-48-refs.t000644 000765 000024 00000003450 13372726265 022553 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; post '/event/update' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => {status => $c->req->json->[0]}); }, 'update'; eval { plugin OpenAPI => {url => 'data://main/with-refs.json'} }; ok !$@, 'resolved refs' or diag $@; my $t = Test::Mojo->new; $t->post_ok('/v1/event/update')->status_is(400); $t->post_ok('/v1/event/update', json => [undef])->status_is(400); $t->post_ok('/v1/event/update', json => ['ok'])->status_is(200)->json_is('/status', 'ok'); done_testing; __DATA__ @@ with-refs.json { "swagger": "2.0", "info": { "description": "Services and stuff", "title": "test api", "version": "0.0.6" }, "schemes": [ "http" ], "host": "localhost", "basePath": "/v1", "paths": { "/event/update": { "$ref": "data://main/spec/event.json#/paths/~1event~1update" } } } @@ spec/event.json { "paths": { "/event/update": { "post": { "operationId": "update", "summary": "Trigger an update to 1 or more properties", "description": "Notify the API of an update that needs processing.", "parameters": [ { "description": "Data structure of events to process", "in": "body", "required": true, "name": "events", "schema": { "$ref": "#/definitions/events" } } ], "responses": { "200": { "$ref": "#/responses/updateSuccess" } } } } }, "responses": { "updateSuccess": { "description": "", "schema": { "type": "object", "required": ["status"] } } }, "definitions": { "events": { "type": "array", "minItems": 1, "items": {"type": "string"} } } } Mojolicious-Plugin-OpenAPI-2.21/t/authenticate.t000644 000765 000024 00000003330 13431336326 022677 0ustar00jhthorsenstaff000000 000000 use 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-2.21/t/yaml.t000644 000765 000024 00000005674 13574517732 021212 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious; my $n = 0; my %modules = ('YAML::XS' => '0.67'); for my $module (keys %modules) { unless (eval "use $module $modules{$module};1") { diag "Skipping test when $module $modules{$module} is not installed"; next; } no warnings qw(once redefine); use JSON::Validator; local *JSON::Validator::_load_yaml = eval "\\\&$module\::Load"; $n++; diag join ' ', $module, $module->VERSION || 0; my $app = Mojolicious->new; $app->routes->get('/pets', sub { }, 'listPets'); $app->routes->get('/pets/:id', sub { }, 'showPetById'); $app->routes->post('/pets', sub { }, 'createPets'); eval { $app->plugin(OpenAPI => {url => 'data://main/coercion.yaml'}); 1 }; ok !$@, "Could not load OpenAPI plugin using $module" or diag $@; } ok 1, 'no yaml modules available' unless $n; done_testing; __DATA__ @@ coercion.yaml --- swagger: "2.0" info: version: 1.0.0 title: Swagger Petstore license: name: MIT host: petstore.swagger.wordnik.com basePath: /v1 schemes: - http consumes: - application/json produces: - application/json paths: /pets: x-something-something: x-nothing-here: No, really! 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) 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: Pets default: description: unexpected error schema: $ref: Error post: summary: Create a pet operationId: createPets tags: - pets responses: 201: description: Null response default: description: unexpected error schema: $ref: Error "/pets/{petId}": get: summary: Info for a specific pet operationId: showPetById tags: - pets parameters: - name: petId in: path description: The id of the pet to retrieve required: true type: string responses: 200: description: Expected response to a valid request schema: $ref: Pets default: description: unexpected error schema: $ref: Error definitions: Pet: required: - id - name properties: id: type: integer format: int64 name: type: string tag: type: string Pets: type: array items: $ref: Pet Error: required: - code - message properties: code: type: integer format: int32 message: type: string Mojolicious-Plugin-OpenAPI-2.21/t/security-disabled.t000644 000765 000024 00000002050 13372726265 023644 0ustar00jhthorsenstaff000000 000000 use 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-2.21/t/discriminator.t000644 000765 000024 00000006305 13372726265 023106 0ustar00jhthorsenstaff000000 000000 use 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-2.21/t/set-request.t000644 000765 000024 00000003355 13372726265 022522 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::Mojo; use Test::More; 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'; plugin OpenAPI => {url => 'data://main/echo.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'); done_testing; __DATA__ @@ echo.json { "swagger": "2.0", "info": { "version": "0.8", "title": "Pets" }, "schemes": [ "http" ], "basePath": "/api", "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" } } } } } } } Mojolicious-Plugin-OpenAPI-2.21/t/collectionformat.t000644 000765 000024 00000005055 13562122170 023566 0ustar00jhthorsenstaff000000 000000 use 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'; plugin OpenAPI => {url => 'data://main/discriminator.json'}; my $t = Test::Mojo->new; # Expected array - got null $t->get_ok('/api/pets')->status_is(400)->json_is('/errors/0/path', '/ri'); # Expected integer - got number. $t->get_ok('/api/pets?ri=1.3')->status_is(400)->json_is('/errors/0/path', '/ri/0'); # Not enough items: 1\/2 $t->get_ok('/api/pets?ri=3&ml=5')->status_is(400)->json_is('/errors/0/path', '/ml'); # Valid $t->get_ok('/api/pets?ri=3&ml=4&ml=2')->status_is(200)->json_is('/ml', [4, 2])->json_is('/ri', [3]); # In path $t->get_ok('/api/pets/ilm,a,r,i')->status_is(200)->json_is('/id', [qw(ilm a r i)]); done_testing; __DATA__ @@ discriminator.json { "swagger" : "2.0", "info" : { "version": "0.8", "title" : "Test collectionFormat" }, "basePath": "/api", "paths" : { "/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" } } } } } } } Mojolicious-Plugin-OpenAPI-2.21/t/path-parameters.t000644 000765 000024 00000002126 13372727221 023322 0ustar00jhthorsenstaff000000 000000 use 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-2.21/t/error-messages.t000644 000765 000024 00000003452 13372726265 023175 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; make_app(); my $t = Test::Mojo->new(MyApp->new); $t->get_ok('/api/user')->status_is(200)->json_is('/age', 42); $t->get_ok('/api/user?code=201')->status_is(501) ->json_is('/errors/0/message', 'No response rule for "201".'); $t->get_ok('/api/user/foo')->status_is(404)->json_is('/errors/0/message', 'Not Found.'); $t->delete_ok('/api/user?code=201')->status_is(501) ->json_is('/errors/0/message', 'Not Implemented.'); 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/user.json'}); } 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); } 1; HERE } __DATA__ @@ user.json { "swagger": "2.0", "info": { "version": "0.8", "title": "Pets" }, "schemes": [ "http" ], "basePath": "/api", "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" } } } } } } } Mojolicious-Plugin-OpenAPI-2.21/t/recursion.t000644 000765 000024 00000003617 13403122304 022226 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::Mojo; use Test::More; use JSON::Validator 'validate_json'; use JSON::Validator::OpenAPI::Mojolicious; my $data = {}; $data->{rec} = $data; $SIG{ALRM} = sub { die 'Recursion!' }; alarm 2; my @errors = ('i_will_be_removed'); eval { @errors = validate_json {top => $data}, 'data://main/spec.json' }; is $@, '', 'no error'; is_deeply(\@errors, [], 'avoided recursion'); # This part of the test checks that we don't go into an infite loop my $validator = JSON::Validator::OpenAPI::Mojolicious->new; is $validator->load_and_validate_schema('data://main/user.json'), $validator, 'load_and_validate_schema no recursion'; is $validator->schema($validator->schema->data), $validator, 'schema() handles $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-2.21/t/v2-security.t000644 000765 000024 00000024016 13520625612 022417 0ustar00jhthorsenstaff000000 000000 use 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'}]}); is_deeply \%checks, {fail1 => 1, pass1 => 1}, 'expected checks occurred'; } { local %checks; $t->post_ok('/api/multiple_fail' => json => {})->status_is(401)->json_is( { 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( { 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'}]}); 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-2.21/t/register.t000644 000765 000024 00000013677 13571755615 022077 0ustar00jhthorsenstaff000000 000000 use 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 => { schema => 'v3', 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'}}} } } } } } } }; ok $obj->route->find('cool_api'), 'found api endpoint'; isa_ok($obj->route, 'Mojolicious::Routes::Route'); isa_ok($obj->validator, 'JSON::Validator::OpenAPI::Mojolicious'); 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 Mojolicious::Plugin::OpenAPI 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 Mojolicious::Plugin::OpenAPI 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); done_testing; __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"] } } } Mojolicious-Plugin-OpenAPI-2.21/t/route-names.t000644 000765 000024 00000006353 13612462550 022470 0ustar00jhthorsenstaff000000 000000 use 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'; $t->get_ok('/api/no-endpoint')->status_is(501)->json_is('/errors/0/message', 'Not Implemented.'); 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-2.21/t/issue-24-booleans-in-yaml-schema.t000644 000765 000024 00000001700 13372726265 026206 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojo::JSON; eval { use Mojolicious::Lite; get('/whatever', sub { shift->render(openapi => {}) }, 'whatever'); plugin OpenAPI => {url => 'data://main/boolean_default.yml'}; 1; } or do { plan skip_all => $@; }; my $t = Test::Mojo->new; $t->get_ok('/api')->status_is(200) ->json_is('/definitions/data/properties/bool_value/default', Mojo::JSON->false); done_testing; __DATA__ @@ boolean_default.yml --- swagger: '2.0' info: version: '0.8' title: Pets schemes: [ http ] basePath: "/api" paths: /whatever: post: x-mojo-name: whatever parameters: - in: body name: body schema: type: object responses: 200: description: Whatever response schema: $ref: '#/definitions/data' definitions: data: type: object properties: bool_value: type: boolean default: false Mojolicious-Plugin-OpenAPI-2.21/t/data/000755 000765 000024 00000000000 13612462655 020754 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OpenAPI-2.21/t/v3-invalid_file_refs_no_path.t000644 000765 000024 00000001565 13571755615 025746 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; use JSON::Validator::OpenAPI::Mojolicious; get '/test' => sub { my $c = shift->openapi->valid_input or return; $c->render(status => 200, openapi => $c->param('pcversion')); }, 'File'; plugin OpenAPI => {schema => 'v3', 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('/definitions') ->content_like(qr!\\/definitions\\/v3-valid_include_yaml-!); my $json = $t->get_ok('/api')->tx->res->body; my $validator = JSON::Validator::OpenAPI::Mojolicious->new(version => 3); eval { $validator->load_and_validate_schema($json, {schema => 'v3'}) }; like $@, qr/Properties not allowed: definitions/, 'load_and_validate_schema fails, wrong placement of data'; done_testing; Mojolicious-Plugin-OpenAPI-2.21/t/ref-param.t000644 000765 000024 00000002317 13372726265 022110 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::Mojo; use Test::More; use Mojolicious::Lite; get '/test' => sub { my $c = shift->openapi->valid_input or return; my $params = $c->validation->output; $c->render(status => 200, openapi => $params->{pcversion}); }, 'File'; plugin OpenAPI => {url => 'data://main/file.yaml'}; my $t = Test::Mojo->new; $t->get_ok('/api/test?x=42')->status_is(200)->content_is('"10.1.0"'); done_testing; __DATA__ @@ file.yaml { "swagger": "2.0", "info": { "title": "Test defaults", "version": "1" }, "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": { "/test": { "get": { "parameters": [ { "$ref": "#/parameters/PCVersion" }, { "name": "x", "in": "query", "type": "string", "description": "x" } ], "operationId": "File", "responses": { "200": { "description": "thing", "schema": { "type": "string" } } } } } } } Mojolicious-Plugin-OpenAPI-2.21/t/tutorial.t000644 000765 000024 00000004155 13425006153 022065 0ustar00jhthorsenstaff000000 000000 use 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-2.21/t/autorender.t000644 000765 000024 00000005652 13425006153 022375 0ustar00jhthorsenstaff000000 000000 use 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.'); $t->post_ok('/api/todo' => json => ['invalid'])->status_is(501) ->json_is('/errors/0/message', 'Not Implemented.'); # 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-2.21/t/v2-file.t000644 000765 000024 00000002113 13402376510 021460 0ustar00jhthorsenstaff000000 000000 use 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 => {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 } ], "responses": { "200": { "description": "ok", "schema": { "type": "object" } } } } } } } Mojolicious-Plugin-OpenAPI-2.21/t/v3-style-array.t000644 000765 000024 00000020505 13612462550 023026 0ustar00jhthorsenstaff000000 000000 use 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', schema => 'v3' }; my $t = Test::Mojo->new; # Expected array - got null $t->get_ok('/api/pets')->status_is(400) ->json_is('/errors/0/path', '/ri'); # Expected integer - got number. $t->get_ok('/api/pets?ri=1.3')->status_is(400) ->json_is('/errors/0/path', '/ri/0'); # Not enough items: 1\/2 $t->get_ok('/api/pets?ri=3&ml=5')->status_is(400) ->json_is('/errors/0/path', '/ml'); # Valid, in path $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]); # Valid, in query $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-2.21/t/v3-default.t000644 000765 000024 00000002027 13571755615 022210 0ustar00jhthorsenstaff000000 000000 use 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 => {schema => 'v3', 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-2.21/t/tutorial_v3.t000644 000765 000024 00000005136 13551721100 022471 0ustar00jhthorsenstaff000000 000000 use 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", schema => 'v3'}); } $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-2.21/t/v3.t000644 000765 000024 00000017743 13555765353 020603 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; 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->render(openapi => $c->param('limit') ? [] : {}); }, 'listPets'; post '/pets' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => '', status => 201); }, 'createPets'; plugin OpenAPI => { url => 'data:///petstore.json', schema => 'v3', 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$}); $t->get_ok('/v1/pets?limit=invalid', {Accept => 'application/json'})->status_is(400) ->json_is('/errors/0/message', 'Expected integer - got string.'); # TODO: Should probably be 400 $t->get_ok('/v1/pets?limit=10', {Accept => 'not/supported'})->status_is(500) ->json_is('/errors/0/message', 'No responses rules defined for Accept not/supported.'); $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', 'Invalid Content-Type.') ->json_is('/errors/1/message', 'Expected integer - got string.'); $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; __DATA__ @@ petstore.json { "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-2.21/t/data/image.jpeg000644 000765 000024 00000000021 13351647463 022700 0ustar00jhthorsenstaff000000 000000 some binary data Mojolicious-Plugin-OpenAPI-2.21/t/spec/bundlecheck.json000644 000765 000024 00000002005 13403122170 024113 0ustar00jhthorsenstaff000000 000000 { "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-2.21/t/spec/v3-invalid_include.yaml000644 000765 000024 00000000274 13571755615 025350 0ustar00jhthorsenstaff000000 000000 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-2.21/t/spec/v3-invalid_file_refs_no_path.yaml000644 000765 000024 00000000524 13571755615 027371 0ustar00jhthorsenstaff000000 000000 openapi: 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-2.21/t/spec/v3-valid_file_refs.yaml000644 000765 000024 00000000564 13571755615 025336 0ustar00jhthorsenstaff000000 000000 openapi: 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-2.21/t/spec/v3-invalid_file_refs.yaml000644 000765 000024 00000000540 13571755615 025657 0ustar00jhthorsenstaff000000 000000 openapi: 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-2.21/t/spec/v3-valid_include.yaml000644 000765 000024 00000000376 13571755615 025024 0ustar00jhthorsenstaff000000 000000 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