OpenAPI-Client-1.09/0000755000076500000240000000000015210277312014001 5ustar jhthorsenstaffOpenAPI-Client-1.09/Changes0000644000076500000240000000767115210277274015316 0ustar jhthorsenstaffRevision history for perl distribution OpenAPI-Client 1.09 2026-06-04T22:34:00 - Fix "Use of uninitialized value $name" warning and dropped query parameters for object parameters with style "deepObject", or "form" with explode jhthorsen/json-validator#282 Contributor: Veesh 1.08 2026-06-04T11:09:00 - Reformulations in the POD documentation (#45) Contributor: Laurent Dami - Autogenerated operation_id #46 Contributor: Askar Timirgazin 1.07 2022-10-26T22:31:59 - No need to run t/00-project.t on install #40 1.06 2022-09-11T09:05:17 - Fix "Use of uninitialized value" warning - Specified Perl version - Updated basic repository files - Updated contributors list 1.05 2022-08-17T09:19:00+0900 - Add support for "collectionFormat" for query params #38 1.04 2022-06-03T09:56:54+0900 - Allow inheritance and roles to be applied before new() #35 #37 Contributor: Veesh Goldman 1.03 2021-10-02T09:27:11+0900 - Fix documentation issues regarding "base_url" #33 - Fix "mojo openapi ..." with 3.0.x spec - Changed "mojo openapi -I" to also be able to dump the whole spec (bundled) 1.02 2021-08-28T16:49:50+0200 - Add support for kebab case in path templating #32 Contributor: Roy Storey 1.01 2021-06-17T12:45:28+0900 - Fix generating correct base URL for OpenAPIv3 schemas 1.00 2021-02-23T15:25:00+0900 - Updated to use new JSON::Validator schema API - Add support for JSON::Validator::Schema::OpenAPIv3 - Removed pre_processor() 0.25 2020-06-30T18:45:37+0900 - Fix setting correct request method on client errors 0.24 2019-05-05T14:36:11+0700 - Fix t/command-local-with-ref.t 0.23 2019-05-05T14:13:14+0700 - Add documentation for option --information|-I - Fix deprecation warning for JSON::Validator->coerce(1) #17 - Fix printing YAML::XS::Boolean as "false" and "true" in dumped documentation - Printed documentation has $ref resolved Can be turned off with JSON_VALIDATOR_REPLACE=0 0.22 2019-01-25T14:45:56+0900 - Updated documentation to use $client instead of $self 0.21 2018-10-03T16:19:00+0900 - Fix failing tests after Mojolicious::Plugin::OpenAPI 2.00 #15 0.20 2018-09-30T22:04:20+0900 - Fix skip tests when getting "Service Unavailable" 0.19 2018-09-26T13:56:14+0900 - Skip tests when getting "Service Unavailable" 0.18 2018-09-02T14:13:51+0200 - Add "after_build_tx" event - Add "opeartionId_p" methods for promise based API 0.17 2018-08-02T16:00:20+0800 - Fix duplicate keys in test #13 0.16 2018-07-06T16:08:35+0800 - Fix "host" can contain port #12 0.15 2018-02-15T22:35:36+0100 - Require JSON::Validator 2.03 0.14 2017-12-23T17:10:22+0100 - Add call_p() that returns a Mojo::Promise 0.13 2017-12-11T19:04:35+0100 - Add default coercion to input parameters - Add call() for calling "invalid" operationIds names - Fix "openapi" client when printing data structures #8 0.12 2017-12-09T11:15:28+0100 - Bumping dependencies 0.11 2017-12-08T14:57:01+0100 - Fix openapi command will not try to decode empty strings #5 Contributor: Ed J 0.10 2017-11-27T18:01:50+0100 - Can resolve $ref in spec - Add support for inheriting "parameters" 0.09 2017-10-04T18:44:23+0200 - Add pre_processor() which can be used to manipulate the request #2 0.08 2017-10-03T11:02:55+0200 - Improved building default base_url() #1 0.07 2017-08-22T21:05:46+0200 - Try to make reading from STDIN a bit more robust 0.06 2017-08-21T09:14:23+0200 - Add validator() method - Add "mojo openapi spec.json -I operationId" - Will list available operationIds when no operationId is specified 0.05 2017-08-20T11:18:54+0200 - Forgot to bump JSON::Validator to 1.01 0.04 2017-08-19T23:05:26+0200 - Fix "mojo openapi" against online resources 0.03 2017-08-19T22:39:33+0200 - Add "mojo openapi" command - Add support for running against local app - Removed local_app() method 0.02 2017-08-19T10:59:07+0200 - Fix validation of body parameters 0.01 2017-08-18T19:30:32+0200 - Converted Swagger2::Client to OpenAPI::Client - Marked as EXPERIMENTAL OpenAPI-Client-1.09/MANIFEST0000644000076500000240000000104215210277312015127 0ustar jhthorsenstaffChanges lib/Mojolicious/Command/openapi.pm lib/OpenAPI/Client.pm Makefile.PL MANIFEST This list of files t/00-project.t t/base-url.t t/body-validation.t t/client.t t/command-local-with-ref.t t/command-local.t t/command-remote.t t/command.t t/json-request.t t/path-templating.t t/spec.json t/spec/dummy.json t/spec/with-external-ref.json t/spec/with-ref.json t/style-explode.t META.yml Module YAML meta-data (added by MakeMaker) META.json Module JSON meta-data (added by MakeMaker) OpenAPI-Client-1.09/t/0000755000076500000240000000000015210277311014243 5ustar jhthorsenstaffOpenAPI-Client-1.09/t/spec.json0000644000076500000240000000134315210277061016073 0ustar jhthorsenstaff{ "swagger": "2.0", "info": { "version": "0.8", "title": "Test client spec" }, "schemes": [ "http" ], "host": "localhost", "basePath": "/v1", "paths": { "/pets/{type}": { "get": { "operationId": "listPets", "x-mojo-to": "listPets", "parameters": [ { "in": "path", "name": "type", "type": "string", "required": true }, { "$ref": "#/parameters/p" } ], "responses": { "200": { "description": "pet response", "schema": { "$ref": "#/definitions/r" } } } } } }, "definitions": { "r": { "type": "array" } }, "parameters": { "p": { "in": "query", "name": "p", "type": "integer" } } } OpenAPI-Client-1.09/t/client.t0000644000076500000240000001621515210277061015715 0ustar jhthorsenstaffuse Mojo::Base -strict; use Mojo::JSON 'true'; use OpenAPI::Client; use Test::More; use Mojolicious::Lite; app->log->level('error') unless $ENV{HARNESS_IS_VERBOSE}; my $i = 0; get '/pets/:type' => sub { $i++; my $c = shift->openapi->valid_input or return; $c->render(openapi => [{type => $c->param('type')}]); }, 'listPetsByType'; get '/pets' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => [$c->req->params->to_hash]); }, 'list pets'; get '/collection-format' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => [$c->req->params->to_hash]); }, 'collectionFormat'; post '/pets' => sub { my $c = shift->openapi->valid_input or return; my $res = $c->req->body_params->to_hash; $res->{dummy} = true if $c->req->headers->header('x-dummy'); $c->render(openapi => $res); }, 'addPet'; plugin OpenAPI => {url => 'data://main/test.json'}; is(OpenAPI::Client->new('data://main/test.json')->base_url, 'http://api.example.com:3000/v1', 'base_url'); is(OpenAPI::Client->new('data://main/test.json')->base_url->host, 'api.example.com', 'base_url host'); is(OpenAPI::Client->new('data://main/test.json')->base_url->port, '3000', 'base_url port'); my $client = OpenAPI::Client->new('data://main/test.json', app => app); my ($obj, $tx); is +ref($client), 'OpenAPI::Client::main_test_json', 'generated class'; isa_ok($client, 'OpenAPI::Client'); can_ok($client, 'addPet'); subtest 'subclassing' => sub { package OpenAPI::Child { use Mojo::Base 'OpenAPI::Client'; sub frobnicate { } } my $old_client = OpenAPI::Client->new('data://main/test.json'); my $new_client = OpenAPI::Child->new('data://main/test.json'); can_ok($new_client, 'frobnicate'); ok(!$old_client->can('frobnicate'), 'does not bleed over'); }; note 'Sync testing'; $tx = $client->listPetsByType; is $tx->res->code, 400, 'sync invalid listPetsByType'; is $tx->error->{message}, 'Invalid input', 'sync invalid message'; is $i, 0, 'invalid on client side'; $tx = $client->listPetsByType({type => 'dog', p => 12}); is $tx->res->code, 200, 'sync listPetsByType'; is $tx->req->url->query->param('p'), 12, 'sync listPetsByType p=12'; is $i, 1, 'one request'; $tx = $client->addPet({age => '5', type => 'dog', name => 'Pluto', 'x-dummy' => true}); is $tx->res->code, 200, 'coercion for "age":"5" works'; ok $tx->remote_address, 'server side response'; $tx = $client->addPet({}); is $tx->req->method, 'POST', 'correct method on invalid input'; ok !$tx->remote_address, 'client side error'; note 'Async testing'; $i = 0; is $client->listPetsByType(sub { ($obj, $tx) = @_; Mojo::IOLoop->stop }), $client, 'async request'; Mojo::IOLoop->start; is $obj, $client, 'got client in callback'; is $tx->res->code, 400, 'invalid listPetsByType'; is $tx->error->{message}, 'Invalid input', 'sync invalid message'; is $i, 0, 'invalid on client side'; note 'Promise testing'; my $p = $client->listPetsByType_p->then(sub { $tx = shift }); $tx = undef; $p->wait; is $tx->res->code, 400, 'invalid listPetsByType'; is $tx->error->{message}, 'Invalid input', 'sync invalid message'; is $i, 0, 'invalid on client side'; note 'call()'; $tx = $client->call('list pets', {page => 2}); is_deeply $tx->res->json, [{page => 2}], 'call(list pets)'; eval { $tx = $client->call('nope') }; like $@, qr{No such operationId.*client\.t}, 'call(nope)'; # this approach from https://metacpan.org/source/SRI/Mojolicious-7.59/t/mojo/promise.t and user_agent.t note 'call_p()'; my $promise = $client->call_p('list pets', {page => 2}); my (@results, @errors); $promise->then(sub { @results = @_ }, sub { @errors = @_ }); $promise->wait; is_deeply $results[0]->res->json, [{page => 2}], 'call_p(list pets)'; is_deeply \@errors, [], 'promise not rejected'; note 'call_p() rejecting'; $promise = $client->call_p('list all pets', {page => 2}); (@results, @errors) = (); $promise->then(sub { @results = @_ }, sub { @errors = @_ }); $promise->wait; is_deeply \@results, [], 'call_p(list all pets) does not exist'; is_deeply \@errors, ['[OpenAPI::Client] No such operationId'], 'promise got rejected'; note 'boolean'; my $err; $client->listPetsByType_p({type => 'cat', is_cool => true})->then(sub { $tx = shift }, sub { $err = shift })->wait; is $tx->res->code, 200, 'accepted is_cool=true'; is $tx->req->url->query->to_string, 'is_cool=1', 'is_cool in query parameter'; subtest 'collectionFormat' => sub { $client->collectionFormat_p({csv => [42, 43, 44]})->then(sub { $tx = shift }, sub { $err = shift })->wait; is $tx->res->code, 200, 'accepted' or diag $tx->res->text; like $tx->req->url->query->to_string, qr{^csv=42[^4]+43[^4]+44$}, 'csv in query parameter'; }; done_testing; __DATA__ @@ test.json { "swagger": "2.0", "info": { "version": "0.8", "title": "Test client spec" }, "schemes": [ "http" ], "host": "api.example.com:3000", "basePath": "/v1", "paths": { "x-whatever": [], "/pets": { "x-whatever": [], "parameters": [ { "$ref": "#/parameters/name" } ], "get": { "operationId": "list pets", "parameters": [ { "in": "query", "name": "page", "type": "integer" } ], "responses": { "200": { "description": "pets", "schema": { "type": "array" } } } }, "post": { "x-whatever": [], "operationId": "addPet", "parameters": [ { "in": "header", "name": "x-dummy", "type": "boolean" }, { "in": "formData", "name": "age", "type": "integer" }, { "in": "formData", "name": "type", "type": "string", "required": true } ], "responses": { "200": { "description": "pet response", "schema": { "type": "object" } } } } }, "/pets/{type}": { "get": { "operationId": "listPetsByType", "parameters": [ { "in": "query", "name": "is_cool", "type": "boolean" }, { "in": "path", "name": "type", "type": "string", "required": true }, { "$ref": "#/parameters/p" } ], "responses": { "200": { "description": "pet response", "schema": { "$ref": "#/definitions/ok" } } } } }, "/collection-format": { "get": { "operationId": "collectionFormat", "parameters": [ {"in": "query", "name": "csv", "type": "array", "items": {"type": "number"}} ], "responses": { "200": { "description": "default collectionFormat when type is array", "schema": { "$ref": "#/definitions/ok" } } } } } }, "parameters": { "name": { "in": "formData", "name": "name", "type": "string" }, "p": { "in": "query", "name": "p", "type": "integer" } }, "definitions": { "ok": { "type": "array" } } } OpenAPI-Client-1.09/t/style-explode.t0000644000076500000240000000707115210277061017235 0ustar jhthorsenstaffuse Mojo::Base -strict; use OpenAPI::Client; use Test::More; use Mojolicious::Lite; app->log->level('error') unless $ENV{HARNESS_IS_VERBOSE}; # Echo the received query parameters straight back, so the test can assert on # what the client actually put on the wire. get '/x' => sub { my $c = shift; $c->render(json => $c->req->url->query->to_hash) }, 'getX'; get '/form' => sub { my $c = shift; $c->render(json => $c->req->url->query->to_hash) }, 'getForm'; # Object parameters serialized with style "deepObject"/"form" and explode are # handed to the validate_request callback with an undefined name, asking for # the whole parameter hash. Regression test for # https://github.com/jhthorsen/json-validator/issues/282 my $client = OpenAPI::Client->new('data://main/test.json', app => app); subtest 'deepObject explode' => sub { my @warnings; local $SIG{__WARN__} = sub { push @warnings, $_[0] }; my $tx = $client->getX({'deep[a]' => 'hello', 'deep[b]' => 'world'}); ok !$tx->res->error, 'request is valid' or diag $tx->res->body; is_deeply $tx->req->url->query->to_hash, {'deep[a]' => 'hello', 'deep[b]' => 'world'}, 'deepObject keys passed through to the query string'; is_deeply \@warnings, [], 'no uninitialized-value warning'; }; subtest 'deepObject explode nested' => sub { my $tx = $client->getX({'deep[a]' => 'x', 'deep[c][gte]' => '1'}); ok !$tx->res->error, 'nested request is valid' or diag $tx->res->body; is_deeply $tx->req->url->query->to_hash, {'deep[a]' => 'x', 'deep[c][gte]' => '1'}, 'nested deepObject keys passed through'; }; subtest 'deepObject explode missing optional param' => sub { my $tx = $client->getX({}); ok !$tx->res->error, 'request without the optional object is valid' or diag $tx->res->body; is $tx->req->url->query->to_string, '', 'no query string when object omitted'; }; subtest 'form explode object' => sub { my @warnings; local $SIG{__WARN__} = sub { push @warnings, $_[0] }; my $tx = $client->getForm({lat => '1.5', lon => '2.5'}); ok !$tx->res->error, 'form-exploded object is valid' or diag $tx->res->body; is_deeply $tx->req->url->query->to_hash, {lat => '1.5', lon => '2.5'}, 'form-exploded object properties passed through'; is_deeply \@warnings, [], 'no uninitialized-value warning'; }; done_testing; __DATA__ @@ test.json { "openapi": "3.0.0", "info": { "title": "style/explode", "version": "0" }, "servers": [{ "url": "http://api.example.com" }], "paths": { "/x": { "get": { "operationId": "getX", "parameters": [ { "name": "deep", "in": "query", "style": "deepObject", "explode": true, "schema": { "type": "object", "properties": { "a": { "type": "string" }, "b": { "type": "string" }, "c": { "type": "object", "properties": { "gte": { "type": "string" } } } } } } ], "responses": { "200": { "description": "ok" } } } }, "/form": { "get": { "operationId": "getForm", "parameters": [ { "name": "loc", "in": "query", "style": "form", "explode": true, "schema": { "type": "object", "properties": { "lat": { "type": "string" }, "lon": { "type": "string" } } } } ], "responses": { "200": { "description": "ok" } } } } } } OpenAPI-Client-1.09/t/spec/0000755000076500000240000000000015210277311015175 5ustar jhthorsenstaffOpenAPI-Client-1.09/t/spec/with-external-ref.json0000644000076500000240000000111715210277061021437 0ustar jhthorsenstaff{ "swagger": "2.0", "info": {"title": "t-app", "version": "0.1.0"}, "basePath": "/ext", "definitions": { "error": {"type": "object"} }, "paths": { "/dummy": { "get": { "operationId": "dummy", "responses": { "200": {"description": "", "schema": {"$ref": "../spec/dummy.json"}}, "201": {"description": "", "schema": {"$ref": "./dummy.json"}}, "202": {"description": "", "schema": {"$ref": "dummy.json"}}, "default": {"description": "Err", "schema":{"$ref": "#/definitions/error"}} } } } } } OpenAPI-Client-1.09/t/spec/dummy.json0000644000076500000240000000002715210277061017224 0ustar jhthorsenstaff{ "type": "object" } OpenAPI-Client-1.09/t/spec/with-ref.json0000644000076500000240000000205715210277061017623 0ustar jhthorsenstaff{ "swagger": "2.0", "info": { "license": { "name": "Apache License, Version 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, "title": "t-app", "version": "0.1.0" }, "basePath": "/api", "produces": [ "application/json" ], "consumes": [ "application/json" ], "responses": { "error": { "description": "Self sufficient", "schema": { "type": "object", "additionalProperties": false, "required": [ "error" ], "properties": { "error": { "type": "string" } } } } }, "paths": { "/t": { "get": { "operationId": "listT", "x-mojo-to": "Controller::OpenAPI::T#list", "tags": [ "t" ], "responses": { "200": { "description": "Self sufficient", "schema": { "type": "array", "items": { "type": "string" } } }, "default": { "$ref": "#/responses/error" } } } } } } OpenAPI-Client-1.09/t/command-local-with-ref.t0000644000076500000240000000165315210277061020670 0ustar jhthorsenstaffuse lib '.'; use Mojo::File 'path'; use Mojolicious; use OpenAPI::Client; use Test::More; my $spec = path(qw(t spec with-ref.json))->to_abs; plan skip_all => 'Cannot read spec' unless -r $spec; $ENV{MOJO_LOG_LEVEL} //= 'warn'; eval { my $app = Mojolicious->new; my $oc; $app->routes->get(sub { my $c = shift }, 'dummy'); $app->plugin(OpenAPI => {default_response_codes => [], spec => $spec}); $app->plugin(OpenAPI => {default_response_codes => [], spec => path(qw(t spec with-external-ref.json))->to_abs}); $oc = OpenAPI::Client->new('/api', app => $app); ok $oc, 'OpenAPI::Client loaded bundled spec' or diag $@; ok $oc->validator->get('/responses/error'), 'responses/error is still there'; $oc = OpenAPI::Client->new('/ext', app => $app); ok $oc, 'OpenAPI::Client loaded bundled spec' or diag $@; } or do { # Getting "Service Unavailable" from some of the cpantesters plan skip_all => $@; }; done_testing; OpenAPI-Client-1.09/t/base-url.t0000644000076500000240000000230615210277061016145 0ustar jhthorsenstaffuse Mojo::Base -strict; use OpenAPI::Client; use Test::More; subtest default => sub { my $client = OpenAPI::Client->new('data://main/test.json'); isa_ok($client->base_url, 'Mojo::URL'); is($client->base_url, 'http://localhost/', 'base_url'); }; subtest constructor => sub { my $client = OpenAPI::Client->new('data://main/test.json', base_url => 'https://example.com/v1/'); isa_ok($client->base_url, 'Mojo::URL'); is($client->base_url, 'https://example.com/v1/', 'base_url'); $client = OpenAPI::Client->new('data://main/test.json', base_url => Mojo::URL->new('https://example.com/v1/')); isa_ok($client->base_url, 'Mojo::URL'); is($client->base_url, 'https://example.com/v1/', 'base_url'); }; subtest attribute => sub { my $client = OpenAPI::Client->new('data://main/test.json'); isa_ok($client->base_url, 'Mojo::URL'); is($client->base_url, 'http://localhost/', 'base_url'); $client->base_url->host('other.example.com')->path('/test'); isa_ok($client->base_url, 'Mojo::URL'); is($client->base_url, 'http://other.example.com/test', 'base_url'); }; done_testing; __DATA__ @@ test.json { "swagger": "2.0", "info": {"version": "0.8", "title": "Test default base_url"}, "paths": {} } OpenAPI-Client-1.09/t/body-validation.t0000644000076500000240000000473515210277061017530 0ustar jhthorsenstaffuse Mojo::Base -strict; use OpenAPI::Client; use Test::More; use Mojo::JSON 'true'; use Mojolicious::Lite; app->log->level('error') unless $ENV{HARNESS_IS_VERBOSE}; my $i = 0; post '/user/login' => sub { $i++; my $c = shift->openapi->valid_input or return; return $c->render(openapi => $c->req->json); }, 'loginUser'; plugin OpenAPI => {url => 'data://main/test.json'}; my $client = OpenAPI::Client->new('data://main/test.json', app => app); my ($tx, $req); $client->once(after_build_tx => sub { $req = pop->req }); $tx = $client->loginUser; is $tx->req, $req, 'after_build_tx emitted tx'; is $tx->res->code, 400, 'invalid loginUser' or diag explain($tx->res->json); is $tx->error->{message}, 'Invalid input', 'invalid message'; $tx = $client->loginUser({body => {email => 'superman@example.com', password => 's3cret'}}); is $tx->res->code, 200, 'valid loginUser' or diag explain($tx->res->json); is $tx->res->json->{email}, 'superman@example.com', 'valid return'; $tx = $client->loginUser({body => {password => 's3cret'}}); is $tx->res->code, 400, 'missing email' or diag explain($tx->res->json); is $tx->error->{message}, 'Invalid input', 'invalid message'; $client->once(after_build_tx => sub { pop->req->headers->header('X-Secret' => 'supersecret') }); $tx = $client->loginUser; is $tx->req->env->{operationId}, 'loginUser', 'got operationId in env'; is $tx->req->headers->header('X-Secret'), 'supersecret', 'modified request'; is $i, 1, 'only sent data to server once'; done_testing; __DATA__ @@ test.json { "swagger": "2.0", "info": { "version": "0.8", "title": "Test client spec" }, "schemes": [ "http" ], "host": "api.example.com", "basePath": "/v1", "paths": { "/user/login": { "post": { "tags": [ "user" ], "summary": "Log in a user based on email and password.", "operationId": "loginUser", "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "type": "object", "required": ["email", "password"], "properties": { "email": { "type": "string", "format": "email", "description": "User email" }, "password": { "type": "string", "description": "User password" } } } } ], "responses": { "200": { "description": "User profile.", "schema": { "type": "object" } } } } } } } OpenAPI-Client-1.09/t/command-local.t0000644000076500000240000000427515210277061017150 0ustar jhthorsenstaffuse Mojo::Base -strict; use Mojo::File 'path'; use Mojolicious::Command::openapi; use Mojolicious; use Test::More; $ENV{MOJO_LOG_LEVEL} //= 'warn'; my @said; Mojo::Util::monkey_patch('Mojolicious::Command::openapi', _say => sub { push @said, @_ }); Mojo::Util::monkey_patch('Mojolicious::Command::openapi', _warn => sub { push @said, @_ }); eval { my $app = Mojolicious->new; $app->routes->post( '/pets' => sub { my $c = shift; my $res = $c->req->json; $res->{key} = $c->param('key'); $c->render(openapi => $res); } )->name('addPet'); $app->plugin('OpenAPI', {url => 'data://main/test.json'}); my $cmd = Mojolicious::Command::openapi->new(app => $app); $cmd->run('/v1'); like "@said", qr{addPet}, 'validated spec from local app'; @said = (); $cmd->run('/v1', 'addPet', -p => "key=abc", -c => '{"type":"dog"}'); like "@said", qr{"key":"abc"}, 'addPet with key'; like "@said", qr{"type":"dog"}, 'addPet with type'; @said = (); my $characters = qq(\x{88c5}\x{903c}\x{4e2d}); my $encoded = Mojo::Util::encode("UTF-8", $characters); $cmd->run('/v1', 'addPet', -p => "key=abc", -c => qq[{"type":"$encoded"}]); like "@said", qr{"key":"abc"}, 'addPet with key'; like "@said", qr{"type":"$characters"}, 'addPet with unicode'; } or do { # Getting "Service Unavailable" from some of the cpantesters plan skip_all => $@; }; done_testing; __DATA__ @@ test.json { "swagger": "2.0", "info": { "version": "0.8", "title": "Test client spec" }, "schemes": [ "http" ], "host": "api.example.com", "basePath": "/v1", "paths": { "/pets": { "post": { "operationId": "addPet", "parameters": [ { "in": "query", "name": "key", "type": "string" }, { "in": "body", "name": "body", "required": true, "schema": { "type": "object", "properties": { "type": { "type": "string", "description": "Type" } } } } ], "responses": { "200": { "description": "pet response", "schema": { "type": "object" } } } } } } } OpenAPI-Client-1.09/t/command.t0000644000076500000240000000314115210277061016047 0ustar jhthorsenstaffuse Mojo::Base -strict; use Mojo::File 'path'; use Test::More; use File::Temp qw(tempfile); use Mojolicious::Command::openapi; my $cmd = Mojolicious::Command::openapi->new; my @said; Mojo::Util::monkey_patch('Mojolicious::Command::openapi', _say => sub { push @said, @_ }); Mojo::Util::monkey_patch('Mojolicious::Command::openapi', _warn => sub { push @said, @_ }); like $cmd->description, qr{Perform Open API requests}, 'description'; like $cmd->usage, qr{APPLICATION openapi SPECIFICATION OPERATION}, 'usage'; eval { $cmd->run }; like $@, qr{APPLICATION openapi SPECIFICATION OPERATION}, 'no arguments'; @said = (); $cmd->run(path('t', 'spec.json')); like $said[0], qr{Operations for http://localhost/v1}, 'validated spec from command line'; like $said[1], qr{^listPets$}m, 'validated spec from command line'; @said = (); $cmd->run(path('t', 'spec.json'), -I => 'listPets'); like "@said", qr{pet response}, 'information about operation'; @said = (); $cmd->run(path('t', 'spec.json'), -I => 'unknown'); like "@said", qr{Could not find}, 'no information about operation'; @said = (); my ($fh, $filename) = tempfile; close $fh; # This is because under Docker, STDIN is !-t, readable immediately, # gives EOF. This simulates that open STDIN, '<', $filename; eval { $cmd->run(path('t', 'spec.json'), 'listPets') }; is $@, ''; like "@said", qr{Missing property}, 'missing property'; @said = (); $cmd->run(path('t', 'spec.json'), 'listPets', '-v'); like "@said", qr{400 Bad Request}, 'verbose'; @said = (); $cmd->run(path('t', 'spec.json'), 'listPets', '/errors/0/path'); like "@said", qr{^/type}, 'json path'; done_testing; OpenAPI-Client-1.09/t/00-project.t0000644000076500000240000000266315210277061016324 0ustar jhthorsenstaffuse strict; use warnings; use Test::More; use File::Find; plan skip_all => 'No such directory: .git' unless $ENV{TEST_ALL} or -d '.git'; plan skip_all => 'HARNESS_PERL_SWITCHES =~ /Devel::Cover/' if +($ENV{HARNESS_PERL_SWITCHES} || '') =~ /Devel::Cover/; for (qw( Test::CPAN::Changes::changes_file_ok+VERSION!4 Test::Pod::Coverage::pod_coverage_ok+VERSION!1 Test::Pod::pod_file_ok+VERSION!1 Test::Spelling::pod_file_spelling_ok+has_working_spellchecker!1 )) { my ($fqn, $module, $sub, $check, $skip_n) = /^((.*)::(\w+))\+(\w+)!(\d+)$/; next if eval "use $module;$module->$check"; no strict qw(refs); *$fqn = sub { SKIP: { skip "$sub(@_) ($module is required)", $skip_n } }; } my @files; find({wanted => sub { /\.pm$/ and push @files, $File::Find::name }, no_chdir => 1}, -e 'blib' ? 'blib' : 'lib'); plan tests => @files * 4 + 4; Test::Spelling::add_stopwords() if Test::Spelling->can('has_working_spellchecker') && Test::Spelling->has_working_spellchecker; for my $file (@files) { my $module = $file; $module =~ s,\.pm$,,; $module =~ s,.*/?lib/,,; $module =~ s,/,::,g; ok eval "use $module; 1", "use $module" or diag $@; Test::Pod::pod_file_ok($file); Test::Pod::Coverage::pod_coverage_ok($module, {also_private => [qr/^[A-Z_]+$/]}); Test::Spelling::pod_file_spelling_ok($file); } Test::CPAN::Changes::changes_file_ok(); __DATA__ Anwar Henning OpenAPI Reneeb Thorsen Veesh listPets operationId ua validator OpenAPI-Client-1.09/t/json-request.t0000644000076500000240000000271115210277061017072 0ustar jhthorsenstaffuse Mojo::Base -strict; use OpenAPI::Client; use Test::More; use Mojolicious::Lite; app->log->level('error') unless $ENV{HARNESS_IS_VERBOSE}; post '/user' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'addUser'; plugin OpenAPI => {url => 'data://main/test.json'}; my $client = OpenAPI::Client->new('data://main/test.json', ua => app->ua); $client->base_url->host(undef)->scheme(undef)->port(undef); is $client->ua, app->ua, 'passed ua as argument, instead of app'; my $tx = $client->addUser({user => {username => 'superwoman'}}); is $tx->res->json->{user}{username}, 'superwoman', 'echo back username (b)'; like $tx->req->headers->header('Content-Type'), qr{application/json}, 'application/json'; undef $tx; my $p = $client->addUser_p({user => {username => 'supergirl'}})->then(sub { $tx = shift }); $p->wait; is $tx->res->json->{user}{username}, 'supergirl', 'echo back username (p)'; done_testing; __DATA__ @@ test.json { "swagger": "2.0", "info": { "version": "0.8", "title": "Test client spec" }, "schemes": [ "http" ], "host": "api.example.com", "basePath": "/v1", "paths": { "/user": { "post": { "operationId": "addUser", "parameters": [ {"in":"body","name":"user","schema":{}} ], "responses": { "200": { "description": "user", "schema": { "type": "object" } } } } } } } OpenAPI-Client-1.09/t/command-remote.t0000644000076500000240000000070215210277061017340 0ustar jhthorsenstaffuse Mojo::Base -strict; use Mojo::File 'path'; use Mojolicious::Command::openapi; use Test::More; plan skip_all => 'TEST_ONLINE=1' unless $ENV{TEST_ONLINE}; my @said; Mojo::Util::monkey_patch('Mojolicious::Command::openapi', _say => sub { push @said, @_ }); my $cmd = Mojolicious::Command::openapi->new; $cmd->run('http://petstore.swagger.io/v2/swagger.json', 'getPetById', -p => 'petId=1'); like "@said", qr{"id":1}, 'getPetById'; done_testing; OpenAPI-Client-1.09/t/path-templating.t0000644000076500000240000000242215210277061017530 0ustar jhthorsenstaffuse Mojo::Base -strict; use OpenAPI::Client; use Test::More; use Mojolicious::Lite; app->log->level('error') unless $ENV{HARNESS_IS_VERBOSE}; post '/user/:user-name' => sub { my $c = shift->openapi->valid_input or return; $c->render(openapi => $c->validation->output); }, 'addUser'; plugin OpenAPI => {url => 'data://main/test.json'}; my $client = OpenAPI::Client->new('data://main/test.json', app => app); my $tx = $client->addUser({'user-name' => 'superwoman'}); is $tx->res->json->{'user-name'}, 'superwoman', 'echo back username (b)'; undef $tx; my $p = $client->addUser_p({'user-name' => 'supergirl'})->then(sub { $tx = shift }); $p->wait; is $tx->res->json->{'user-name'}, 'supergirl', 'echo back username (p)'; done_testing; __DATA__ @@ test.json { "swagger": "2.0", "info": { "version": "0.8", "title": "Test client spec" }, "schemes": [ "http" ], "host": "api.example.com", "basePath": "/v1", "paths": { "/user/{user-name}": { "post": { "operationId": "addUser", "parameters": [ { "in": "path", "name": "user-name", "required": true, "type": "string" } ], "responses": { "200": { "description": "user", "schema": { "type": "object" } } } } } } } OpenAPI-Client-1.09/META.yml0000644000076500000240000000255215210277312015256 0ustar jhthorsenstaff--- abstract: 'A client for talking to an Open API powered server' author: - 'Jan Henning Thorsen ' build_requires: ExtUtils::MakeMaker: '0' Test::More: '0.88' configure_requires: ExtUtils::MakeMaker: '0' dynamic_config: 0 generated_by: 'ExtUtils::MakeMaker version 7.76, 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: OpenAPI-Client no_index: directory: - t - inc - examples - t requires: JSON::Validator: '5.09' Mojolicious::Plugin::OpenAPI: '5.05' perl: '5.016' resources: IRC: url: irc://irc.libera.chat/#convos web: https://web.libera.chat/#convos bugtracker: https://github.com/jhthorsen/openapi-client/issues homepage: https://github.com/jhthorsen/openapi-client license: http://www.opensource.org/licenses/artistic-license-2.0 repository: https://github.com/jhthorsen/openapi-client.git version: '1.09' x_contributors: - 'Clive Holloway ' - 'Ed J ' - 'Jan Henning Thorsen ' - 'Mohammad S Anwar ' - 'Reneeb ' - 'Roy Storey ' - 'Veesh Goldman ' x_serialization_backend: 'CPAN::Meta::YAML version 0.020' OpenAPI-Client-1.09/lib/0000755000076500000240000000000015210277311014546 5ustar jhthorsenstaffOpenAPI-Client-1.09/lib/OpenAPI/0000755000076500000240000000000015210277311016001 5ustar jhthorsenstaffOpenAPI-Client-1.09/lib/OpenAPI/Client.pm0000644000076500000240000003676515210277307017603 0ustar jhthorsenstaffpackage OpenAPI::Client; use Mojo::EventEmitter -base; use Carp (); use JSON::Validator; use Mojo::UserAgent; use Mojo::Promise; use Scalar::Util qw(blessed); use constant DEBUG => $ENV{OPENAPI_CLIENT_DEBUG} || 0; our $VERSION = '1.09'; has base_url => sub { my $self = shift; my $validator = $self->validator; my $url = $validator->can('base_url') ? $validator->base_url->clone : Mojo::URL->new; $url->scheme('http') unless $url->scheme; $url->host('localhost') unless $url->host; return $url; }; has ua => sub { Mojo::UserAgent->new }; sub call { my ($self, $op) = (shift, shift); my $code = $self->can($op) or Carp::croak('[OpenAPI::Client] No such operationId'); return $self->$code(@_); } sub call_p { my ($self, $op) = (shift, shift); my $code = $self->can("${op}_p") or return Mojo::Promise->reject('[OpenAPI::Client] No such operationId'); return $self->$code(@_); } sub new { my ($parent, $specification) = (shift, shift); my $attrs = @_ == 1 ? shift : {@_}; my $class = $parent->_url_to_class($specification); $parent->_generate_class($class, $specification, $attrs) unless $class->isa($parent); my $self = $class->SUPER::new($attrs); $self->base_url(Mojo::URL->new($self->{base_url})) if $self->{base_url} and !blessed $self->{base_url}; $self->ua->transactor->name('Mojo-OpenAPI (Perl)') unless $self->{ua}; if (my $app = delete $self->{app}) { $self->base_url->host(undef)->scheme(undef)->port(undef); $self->ua->server->app($app); } return $self; } sub validator { Carp::confess("validator() is not defined for $_[0]") } sub _generate_class { my ($parent, $class, $specification, $attrs) = @_; my $jv = JSON::Validator->new; $jv->coerce($attrs->{coerce} // 'booleans,numbers,strings'); $jv->store->ua->server->app($attrs->{app}) if $attrs->{app}; my $schema = $jv->schema($specification)->schema; die "Invalid schema: $specification has the following errors:\n", join "\n", @{$schema->errors} if @{$schema->errors}; eval <<"HERE" or Carp::confess("package $class: $@"); package $class; use Mojo::Base '$parent'; 1; HERE Mojo::Util::monkey_patch($class => validator => sub {$schema}); return unless $schema->can('routes'); # In case it is not an OpenAPI spec for my $route ($schema->routes->each) { my $operation_id = $route->{operation_id}; unless ( $route->{operation_id} ) { $operation_id = join '_', $route->{method}, $route->{path}; $operation_id =~ s|\{[^}]+\}|_|g; $operation_id =~ s|[/-]|_|g; $operation_id =~ s|__+|_|g; } warn "[$class] Add method $operation_id() for $route->{method} $route->{path}\n" if DEBUG; $class->_generate_method_bnb($operation_id => $route); $class->_generate_method_p("${operation_id}_p" => $route); } } sub _generate_method_bnb { my ($class, $method_name, $route) = @_; Mojo::Util::monkey_patch $class => $method_name => sub { my $cb = ref $_[-1] eq 'CODE' ? pop : undef; my $self = shift; my $tx = $self->_build_tx($route, @_); if ($tx->error) { return $tx unless $cb; Mojo::IOLoop->next_tick(sub { $self->$cb($tx) }); return $self; } return $self->ua->start($tx) unless $cb; $self->ua->start($tx, sub { $self->$cb($_[1]) }); return $self; }; } sub _generate_method_p { my ($class, $method_name, $route) = @_; Mojo::Util::monkey_patch $class => $method_name => sub { my $self = shift; my $tx = $self->_build_tx($route, @_); return $self->ua->start_p($tx) unless my $err = $tx->error; return Mojo::Promise->new->reject($err->{message}) unless $err->{code}; return Mojo::Promise->new->reject('WebSocket handshake failed') if $tx->req->is_handshake && !$tx->is_websocket; return Mojo::Promise->new->resolve($tx); }; } sub _build_tx { my ($self, $route, $params, %content) = @_; my $v = $self->validator; my $url = $self->base_url->clone; my ($tx, %headers); push @{$url->path}, map { local $_ = $_; s,\{([-\w]+)\},{$params->{$1}//''},ge; $_ } grep {length} split '/', $route->{path}; my @errors = $self->validator->validate_request( [@$route{qw(method path)}], { body => sub { my ($name, $param) = @_; if (exists $params->{$name}) { $content{json} = $params->{$name}; } else { for ('body', sort keys %{$self->ua->transactor->generators}) { next unless exists $content{$_}; $params->{$name} = $content{$_}; last; } } return {exists => $params->{$name}, value => $params->{$name}}; }, formData => sub { my ($name, $param) = @_; my $value = _param_as_array($name => $params); $content{form}{$name} = $params->{$name}; return {exists => !!@$value, value => $value}; }, header => sub { my ($name, $param) = @_; my $value = _param_as_array($name => $params); $headers{$name} = $value; return {exists => !!@$value, value => $value}; }, path => sub { my ($name, $param) = @_; return {exists => exists $params->{$name}, value => $params->{$name}}; }, query => sub { my ($name, $param) = @_; # An undefined name means JSON::Validator wants the whole parameter # hash so it can reassemble an exploded object parameter (style # "deepObject", or "form" with explode). The matching keys are passed # through to the query string as-is, while the complete hash is handed # back for validation. See # JSON::Validator::Schema::OpenAPIv3::_get_parameter_value. unless (defined $name) { my @keys = +($param->{style} // '') eq 'deepObject' ? grep {/^\Q$param->{name}\E\[/} keys %$params : grep { exists $params->{$_} } keys %{$param->{schema}{properties} || {}}; $url->query->param($_ => $params->{$_}) for sort @keys; return {exists => @keys ? 1 : 0, value => {%$params}}; } my $value = _param_as_array($name => $params); $url->query->param($name => _coerce_collection_format($value, $param)); return {exists => !!@$value, value => $value}; }, } ); if (@errors) { warn "[@{[ref $self]}] Validation for $route->{method} $url failed: @errors\n" if DEBUG; $tx = Mojo::Transaction::HTTP->new; $tx->req->method(uc $route->{method}); $tx->req->url($url); $tx->res->headers->content_type('application/json'); $tx->res->body(Mojo::JSON::encode_json({errors => \@errors})); $tx->res->code(400)->message($tx->res->default_message); $tx->res->error({message => 'Invalid input', code => 400}); } else { warn "[@{[ref $self]}] Validation for $route->{method} $url was successful\n" if DEBUG; $tx = $self->ua->build_tx($route->{method}, $url, \%headers, defined $content{body} ? $content{body} : %content); } $tx->req->env->{operationId} = $route->{operation_id}; $self->emit(after_build_tx => $tx); return $tx; } sub _coerce_collection_format { my ($value, $param) = @_; my $format = $param->{collectionFormat} || (+($param->{type} // '') eq 'array' ? 'csv' : ''); return $value if !$format or $format eq 'multi'; return join "|", @$value if $format eq 'pipes'; return join " ", @$value if $format eq 'ssv'; return join "\t", @$value if $format eq 'tsv'; return join ",", @$value; } sub _param_as_array { my ($name, $params) = @_; return !exists $params->{$name} ? [] : ref $params->{$name} eq 'ARRAY' ? $params->{$name} : [$params->{$name}]; } sub _url_to_class { my ($self, $package) = @_; $package =~ s!^\w+?://!!; $package =~ s!\W!_!g; $package = Mojo::Util::md5_sum($package) if length $package > 110; # 110 is a bit random, but it cannot be too long return "$self\::$package"; } 1; =encoding utf8 =head1 NAME OpenAPI::Client - A client for talking to an Open API powered server =head1 DESCRIPTION L generates objects that can talk to Open API servers. For each fresh OpenAPI contract given to the L method, a custom subclass is created from the Open API specification, with methods corresponding to the operationIds. Parameters received by these methods are transformed into HTTP requests. Input validation is performed, so invalid data won't be sent to the server. Note that this implementation is currently EXPERIMENTAL, but unlikely to change! Feedback is appreciated. =head1 SYNOPSIS =head2 Creating a client use OpenAPI::Client; $client = OpenAPI::Client->new("file:///path/to/api.json"); The specification given to L must point to a valid OpenAPI document. Several syntax variants are admitted -- see the L method. =head2 Open API specification The OpenAPI document can be in OpenAPI version v2.x or v3.x, and it can be in either JSON or YAML format. Example: openapi: 3.0.1 info: title: Swagger Petstore version: 1.0.0 servers: - url: http://petstore.swagger.io/v1 paths: /pets: get: operationId: listPets ... The url specified in the OpenAPI document can be altered at any time, if you need to send data to another custom endpoint -- see the L method. =head2 Client The OpenAPI API specification will be used to generate a sub-class of L with additional methods corresponding to "operationId" inside of each path definition : # Blocking $tx = $client->listPets; # Non-blocking $client = $client->listPets(sub { my ($client, $tx) = @_; }); # Promises $promise = $client->listPets_p->then(sub { my $tx = shift }); # With parameters $tx = $client->listPets({limit => 10}); See L for more information about what you can do with the C<$tx> object. Often you just want something like this: # Check for errors die $tx->error->{message} if $tx->error; # Extract data from the JSON responses say $tx->res->json->{pets}[0]{name}; Check out L, L and L for some of the most used methods in that class. =head1 CUSTOMIZATION =head2 Custom server URL If you want to request another server than the one specified in the Open API document, you can change the L: # Pass on a Mojo::URL object to the constructor $base_url = Mojo::URL->new("http://example.com"); $client1 = OpenAPI::Client->new("file:///path/to/api.json", base_url => $base_url); # A plain string will be converted to a Mojo::URL object $client2 = OpenAPI::Client->new("file:///path/to/api.json", base_url => "http://example.com"); # Change the base_url after the client has been created $client3 = OpenAPI::Client->new("file:///path/to/api.json"); $client3->base_url->host("other.example.com"); =head2 Custom content You can send XML or any format you like, but this requires you to add a new "generator": use Your::XML::Library "to_xml"; $client->ua->transactor->add_generator(xml => sub { my ($t, $tx, $data) = @_; $tx->req->body(to_xml $data); return $tx; }); $client->addHero({}, xml => {name => "Supergirl"}); See L for more details. =head1 EVENTS =head2 after_build_tx $client->on(after_build_tx => sub { my ($client, $tx) = @_ }) This event is emitted after a L object has been built, just before it is passed on to the L. Note that all validation has already been run, so altering the C<$tx> too much might cause an invalid request on the server side. A special L variable will be set, to reference the operationId: $tx->req->env->{operationId}; Note that this usage of C is currently EXPERIMENTAL: =head1 ATTRIBUTES =head2 base_url $base_url = $client->base_url; Returns a L object with the base URL to the API. The default value comes from C, C and C in the OpenAPI v2 specification or from C in the OpenAPI v3 specification. =head2 ua $ua = $client->ua; Returns a L object which is used to execute requests. =head1 CLASS METHODS =head2 new $client = OpenAPI::Client->new($specification, \%attributes); $client = OpenAPI::Client->new($specification, %attributes); Returns an object of a dynamic subclass of C, with methods generated from the OpenAPI specification located at C<$specification>. Such subclasses are cached, so several invocations of C with the same C<$specification> URL will result in several instances of the same subclass. The C<$specification> argument can accept various syntaxes -- see L. Extra C<%attributes> can be: =over 2 =item app Can be used to run against a local L instance instead of issuing real HTTP calls to a remote server. =item coerce See L. Default to "booleans,numbers,strings". =back =head1 INSTANCE METHODS =head2 call $tx = $client->call($operationId => \%params, %content); $client = $client->call($operationId => \%params, %content, sub { my ($client, $tx) = @_; }); Used to call an C<$operationId> that is not a proper Perl method name, such as "list pets" instead of "listPets", or to check if an C<$operationId> is supported. Unsupported operationIds throw an exception matching text "No such operationId". C<$operationId> is the name of the resource defined in the L. C<$params> is optional, but must be a hash ref, where the keys should match a named parameter in the L. C<%content> is used for the body of the request, where the key needs to be either "body" or a matching L. Example: $client->addHero({}, body => "Some data"); $client->addHero({}, json => {name => "Supergirl"}); Like in L, an additional coderef argument can be supplied in last position. In that case the call is asynchronous, and the coderef will be called as a continuation callback, with two arguments C<$client> (the current openAPI client) and C<$tx> (a L object). See L for details. =head2 call_p $promise = $client->call_p($operationId => $params, %content); $promise->then(sub { my $tx = shift }); As L above, but returns a L object. =head2 validator $validator = $client->validator; $validator = $class->validator; Returns the L object associated with the current generated class. Depending on the openAPI specification, this object will belong to the L or L subclass. This object global to the class, so changing it will affect all instances returned by L. =head1 COPYRIGHT AND LICENSE Copyright (C) 2017-2021, 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 AUTHORS =head2 Project Founder Jan Henning Thorsen - C =head2 Contributors =over 2 =item * Clive Holloway =item * Ed J =item * Jan Henning Thorsen =item * Jan Henning Thorsen =item * Mohammad S Anwar =item * Reneeb =item * Roy Storey =item * Veesh Goldman =item * Laurent Dami =back =cut OpenAPI-Client-1.09/lib/Mojolicious/0000755000076500000240000000000015210277311017042 5ustar jhthorsenstaffOpenAPI-Client-1.09/lib/Mojolicious/Command/0000755000076500000240000000000015210277311020420 5ustar jhthorsenstaffOpenAPI-Client-1.09/lib/Mojolicious/Command/openapi.pm0000644000076500000240000001355315210277307022425 0ustar jhthorsenstaffpackage Mojolicious::Command::openapi; use Mojo::Base 'Mojolicious::Command'; use OpenAPI::Client; use Mojo::JSON qw(encode_json decode_json j); use Mojo::Util qw(encode getopt); use constant YAML => eval 'require YAML::XS;1'; use constant REPLACE => $ENV{JSON_VALIDATOR_REPLACE} // 1; sub _say { length && say encode('UTF-8', $_) for @_ } sub _warn { warn @_ } has description => 'Perform Open API requests'; has usage => sub { shift->extract_usage . "\n" }; has _client => undef; sub run { my ($self, @args) = @_; my %ua; getopt \@args, 'i|inactivity-timeout=i' => sub { $ua{inactivity_timeout} = $_[1] }, 'I|information' => \my $info, 'o|connect-timeout=i' => sub { $ua{connect_timeout} = $_[1] }, 'p|parameter=s' => \my %parameters, 'c|content=s' => \my $content, 'S|response-size=i' => sub { $ua{max_response_size} = $_[1] }, 'v|verbose' => \my $verbose; # Read body from STDIN vec(my $r, fileno(STDIN), 1) = 1; $content //= !-t STDIN && select($r, undef, undef, 0) ? join '', : undef; my @client_args = (shift @args); my $op = shift @args; my $selector = shift @args // ''; die $self->usage unless $client_args[0]; if ($client_args[0] =~ m!^/! and !-e $client_args[0]) { $client_args[0] = Mojo::URL->new($client_args[0]); push @client_args, app => $self->app; } $self->_client(OpenAPI::Client->new(@client_args)); return $self->_info($op) if $info; return $self->_list unless $op; die qq(Unknown operationId "$op".\n) unless $self->_client->can($op); $self->_client->ua->proxy->detect unless $ENV{OPENAPI_NO_PROXY}; $self->_client->ua->$_($ua{$_}) for keys %ua; $self->_client->ua->on( start => sub { my ($ua, $tx) = @_; weaken $tx; $tx->res->content->on(body => sub { _warn _header($tx->req), _header($tx->res) }) if $verbose; } ); my $tx = $self->_client->call($op => \%parameters, $content ? (json => decode_json $content) : ()); if ($tx->error and $tx->error->{message} eq 'Invalid input') { _warn _header($tx->req), _header($tx->res) if $verbose; } return _json($tx->res->json, $selector) if !length $selector || $selector =~ m!^/!; return _say $tx->res->dom->find($selector)->each; } sub _header { $_[0]->build_start_line, $_[0]->headers->to_string, "\n\n" } sub _info { my ($self, $op) = @_; local $YAML::XS::Boolean = 'JSON::PP'; unless ($op) { my $op_spec = $self->_client->validator->bundle->data; return _say YAML ? YAML::XS::Dump($op_spec) : Mojo::Util::dumper($op_spec); } my ($schema, $op_spec) = ($self->_client->validator); for my $route ($schema->routes->each) { next if !$route->{operation_id} or $route->{operation_id} ne $op; $op_spec = $schema->get(['paths', @$route{qw(path method)}]); } return _warn qq(Could not find the given operationId "$op".\n) unless $op_spec; local $YAML::XS::Boolean = 'JSON::PP'; return _say YAML ? YAML::XS::Dump($op_spec) : Mojo::Util::dumper($op_spec); } sub _json { return unless defined(my $data = Mojo::JSON::Pointer->new(shift)->get(shift)); return _say $data unless ref $data eq 'HASH' || ref $data eq 'ARRAY'; _say Mojo::Util::decode('UTF-8', encode_json $data); } sub _list { my $self = shift; _warn "--- Operations for @{[$self->_client->base_url]}\n"; $_->{operation_id} && _say $_->{operation_id} for $self->_client->validator->routes->each; } 1; =encoding utf8 =head1 NAME Mojolicious::Command::openapi - Perform Open API requests =head1 SYNOPSIS Usage: APPLICATION openapi SPECIFICATION OPERATION "{ARGUMENTS}" [SELECTOR|JSON-POINTER] # Fetch /api from myapp.pl and list available operationId ./myapp.pl openapi /api # Dump the whole specification or for an operationId ./myapp.pl openapi /api -I ./myapp.pl openapi /api -I addPet # Run an operation against a local application ./myapp.pl openapi /api listPets /pets/0 # Run an operation against a local application, with body parameter ./myapp.pl openapi /api addPet -c '{"name":"pluto"}' echo '{"name":"pluto"} | ./myapp.pl openapi /api addPet # Run an operation with parameters mojo openapi spec.json listPets -p limit=10 -p type=dog # Run against local or online specifications mojo openapi /path/to/spec.json listPets mojo openapi http://service.example.com/api.json listPets Options: -h, --help Show this summary of available options -c, --content JSON content, with body parameter data -i, --inactivity-timeout Inactivity timeout, defaults to the value of MOJO_INACTIVITY_TIMEOUT or 20 -I, --information [operationId] Dump the specification about a given operationId or the whole spec. YAML::XS is preferred if available. -o, --connect-timeout Connect timeout, defaults to the value of MOJO_CONNECT_TIMEOUT or 10 -p, --parameter Specify multiple header, path, or query parameter -S, --response-size Maximum response size in bytes, defaults to 2147483648 (2GB) -v, --verbose Print request and response headers to STDERR =head1 DESCRIPTION L is a command line interface for L. Not that this implementation is currently EXPERIMENTAL! Feedback is appreciated. =head1 ATTRIBUTES =head2 description $str = $command->description; =head2 usage $str = $command->usage; =head1 METHODS =head2 run $command->run(@ARGV); Run this command. =head1 SEE ALSO L. =cut OpenAPI-Client-1.09/Makefile.PL0000644000076500000240000000343115210277061015755 0ustar jhthorsenstaffuse 5.016; use strict; use warnings; use ExtUtils::MakeMaker; my $GITHUB_URL = 'https://github.com/jhthorsen/openapi-client'; my %WriteMakefileArgs = ( NAME => 'OpenAPI::Client', AUTHOR => 'Jan Henning Thorsen ', LICENSE => 'artistic_2', ABSTRACT_FROM => 'lib/OpenAPI/Client.pm', VERSION_FROM => 'lib/OpenAPI/Client.pm', TEST_REQUIRES => {'Test::More' => '0.88'}, PREREQ_PM => {'JSON::Validator' => '5.09', 'Mojolicious::Plugin::OpenAPI' => '5.05'}, META_MERGE => { 'dynamic_config' => 0, 'meta-spec' => {version => 2}, 'no_index' => {directory => [qw(examples t)]}, 'prereqs' => {runtime => {requires => {perl => '5.016'}}}, 'resources' => { bugtracker => {web => "$GITHUB_URL/issues"}, homepage => $GITHUB_URL, license => ['http://www.opensource.org/licenses/artistic-license-2.0'], repository => {type => 'git', url => "$GITHUB_URL.git", web => $GITHUB_URL}, x_IRC => {url => 'irc://irc.libera.chat/#convos', web => 'https://web.libera.chat/#convos'}, }, 'x_contributors' => [ 'Clive Holloway ', 'Ed J ', 'Jan Henning Thorsen ', 'Mohammad S Anwar ', 'Reneeb ', 'Roy Storey ', 'Veesh Goldman ', ], }, 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); OpenAPI-Client-1.09/META.json0000644000076500000240000000416215210277312015425 0ustar jhthorsenstaff{ "abstract" : "A client for talking to an Open API powered server", "author" : [ "Jan Henning Thorsen " ], "dynamic_config" : 0, "generated_by" : "ExtUtils::MakeMaker version 7.76, CPAN::Meta::Converter version 2.150010", "license" : [ "artistic_2" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : 2 }, "name" : "OpenAPI-Client", "no_index" : { "directory" : [ "t", "inc", "examples", "t" ] }, "prereqs" : { "build" : { "requires" : { "ExtUtils::MakeMaker" : "0" } }, "configure" : { "requires" : { "ExtUtils::MakeMaker" : "0" } }, "runtime" : { "requires" : { "JSON::Validator" : "5.09", "Mojolicious::Plugin::OpenAPI" : "5.05", "perl" : "5.016" } }, "test" : { "requires" : { "Test::More" : "0.88" } } }, "release_status" : "stable", "resources" : { "bugtracker" : { "web" : "https://github.com/jhthorsen/openapi-client/issues" }, "homepage" : "https://github.com/jhthorsen/openapi-client", "license" : [ "http://www.opensource.org/licenses/artistic-license-2.0" ], "repository" : { "type" : "git", "url" : "https://github.com/jhthorsen/openapi-client.git", "web" : "https://github.com/jhthorsen/openapi-client" }, "x_IRC" : { "url" : "irc://irc.libera.chat/#convos", "web" : "https://web.libera.chat/#convos" } }, "version" : "1.09", "x_contributors" : [ "Clive Holloway ", "Ed J ", "Jan Henning Thorsen ", "Mohammad S Anwar ", "Reneeb ", "Roy Storey ", "Veesh Goldman " ], "x_serialization_backend" : "JSON::PP version 4.16" }