diff options
| -rw-r--r-- | eg/api.pl | 2 | ||||
| -rw-r--r-- | lib/Net/HTTP/Spore.pm | 108 | ||||
| -rw-r--r-- | lib/Net/HTTP/Spore/Meta.pm | 1 | ||||
| -rw-r--r-- | lib/Net/HTTP/Spore/Meta/Class.pm | 2 | ||||
| -rw-r--r-- | lib/Net/HTTP/Spore/Meta/Method.pm | 51 | ||||
| -rw-r--r-- | lib/Net/HTTP/Spore/Meta/Method/Spore.pm | 6 | ||||
| -rw-r--r-- | lib/Net/HTTP/Spore/Role/Debug.pm | 14 | ||||
| -rw-r--r-- | lib/Net/HTTP/Spore/Role/Description.pm | 7 | ||||
| -rw-r--r-- | lib/Net/HTTP/Spore/Role/Request.pm | 1 | ||||
| -rw-r--r-- | t/specs/api2.json | 20 | ||||
| -rw-r--r-- | t/spore-method/payload.t | 33 | ||||
| -rw-r--r-- | t/spore/01_new_from_string.t | 68 |
12 files changed, 261 insertions, 52 deletions
@@ -8,6 +8,6 @@ my $api = Net::HTTP::Spore->new_from_spec(shift); $api->enable('Net::HTTP::Spore::Middleware::Format::JSON'); -my ( $content, $result ) = $api->user_information( format => 'json', username => 'franckcuny' ); +my ( $content, $result ) = $api->get_info( format => 'json', username => 'franckcuny' ); say "name => ". $content->{body}->{user}->{name}; diff --git a/lib/Net/HTTP/Spore.pm b/lib/Net/HTTP/Spore.pm index 34f4740..03606a0 100644 --- a/lib/Net/HTTP/Spore.pm +++ b/lib/Net/HTTP/Spore.pm @@ -8,70 +8,124 @@ use IO::All; use JSON; use Carp; use Try::Tiny; +use Scalar::Util; use Net::HTTP::Spore::Core; our $VERSION = 0.01; +# XXX should we let the possibility to override this super class, or add +# another superclasses? + sub new_from_string { my ($class, $string, %args) = @_; - my $spec; + my $spore_class = + Class::MOP::Class->create_anon_class( + superclasses => ['Net::HTTP::Spore::Core'] ); - try { - $spec = JSON::decode_json($string); - }catch{ - Carp::confess("unable to parse JSON spec: ".$_); - }; + my $spore_object = _attach_spec_to_class($string, \%args, $spore_class); + + return $spore_object; +} + +sub new_from_strings { + my $class = shift; - my ( $spore_class, $spore_object ); - # XXX should we let the possibility to override this super class, or add - # another superclasses? + my $opts; + if (ref ($_[-1]) eq 'HASH') { + $opts = pop @_; + } + my @strings = @_; - $spore_class = + my $spore_class = Class::MOP::Class->create_anon_class( superclasses => ['Net::HTTP::Spore::Core'] ); + my $spore_object = undef; + foreach my $string (@strings) { + $spore_object = _attach_spec_to_class($string, $opts, $spore_class, $spore_object); + } + return $spore_object; +} + +sub new_from_spec { + my ( $class, $spec_file, %args ) = @_; + + Carp::confess("specification file is missing") unless $spec_file; + + my $content = _read_spec($spec_file); + + $class->new_from_string( $content, %args ); +} + +sub new_from_specs { + my $class = shift; + + my $opts; + if (ref ($_[-1]) eq 'HASH') { + $opts = pop @_; + } + my @specs = @_; + + my @strings; + foreach my $spec (@specs) { + push @strings,_read_spec($spec); + } + + $class->new_from_strings(@strings, $opts); +} + +sub _attach_spec_to_class { + my ( $string, $opts, $class, $object ) = @_; + + my $spec; + try { + $spec = JSON::decode_json($string); + } + catch { + Carp::confess( "unable to parse JSON spec: " . $_ ); + }; + try { my $base_url; - if ( $spec->{base_url} && !$args{base_url} ) { - $args{base_url} = $spec->{base_url}; + if ( $spec->{base_url} && !$opts->{base_url} ) { + $opts->{base_url} = $spec->{base_url}; } - elsif ( !$args{base_url} ) { + elsif ( !$opts->{base_url} ) { die "base_url is missing!"; } if ( $spec->{formats} ) { - $args{formats} = $spec->{formats}; + $opts->{formats} = $spec->{formats}; } if ( $spec->{authentication} ) { - $args{authentication} = $spec->{authentication}; + $opts->{authentication} = $spec->{authentication}; } - $spore_object = $spore_class->new_object(%args); - $spore_object = _add_methods( $spore_object, $spec->{methods} ); - + if ( !$object ) { + $object = $class->new_object(%$opts); + } + $object = _add_methods( $object, $spec->{methods} ); } catch { Carp::confess( "unable to create new Net::HTTP::Spore object: " . $_ ); }; - return $spore_object; + return $object; } -sub new_from_spec { - my ( $class, $spec_file, %args ) = @_; - - Carp::confess("specification file is missing") unless $spec_file; +sub _read_spec { + my $spec_file = shift; - my ( $content, $spec ); + my $content; if ( $spec_file =~ m!^http(s)?://! ) { my $uri = URI->new($spec_file); - my $req = HTTP::Request->new(GET => $spec_file); + my $req = HTTP::Request->new( GET => $spec_file ); my $ua = LWP::UserAgent->new(); - my $res = $ua->request( $req ); + my $res = $ua->request($req); $content = $res->content; } else { @@ -81,7 +135,7 @@ sub new_from_spec { $content < io($spec_file); } - $class->new_from_string( $content, %args ); + return $content; } sub _add_methods { diff --git a/lib/Net/HTTP/Spore/Meta.pm b/lib/Net/HTTP/Spore/Meta.pm index ec773b6..14b4266 100644 --- a/lib/Net/HTTP/Spore/Meta.pm +++ b/lib/Net/HTTP/Spore/Meta.pm @@ -36,6 +36,7 @@ sub init_meta { for => $for, roles => [ qw/ + Net::HTTP::Spore::Role::Debug Net::HTTP::Spore::Role::Description Net::HTTP::Spore::Role::UserAgent Net::HTTP::Spore::Role::Request diff --git a/lib/Net/HTTP/Spore/Meta/Class.pm b/lib/Net/HTTP/Spore/Meta/Class.pm index 4ddd5c6..7571305 100644 --- a/lib/Net/HTTP/Spore/Meta/Class.pm +++ b/lib/Net/HTTP/Spore/Meta/Class.pm @@ -4,7 +4,7 @@ package Net::HTTP::Spore::Meta::Class; use Moose::Role; -with qw/Net::HTTP::Spore::Meta::Method::Spore/; +with qw/Net::HTTP::Spore::Meta::Method::Spore Net::HTTP::Spore::Role::Debug/; 1; diff --git a/lib/Net/HTTP/Spore/Meta/Method.pm b/lib/Net/HTTP/Spore/Meta/Method.pm index b546d74..10132ed 100644 --- a/lib/Net/HTTP/Spore/Meta/Method.pm +++ b/lib/Net/HTTP/Spore/Meta/Method.pm @@ -50,12 +50,20 @@ has path => ( is => 'ro', isa => 'UriPath', required => 1 ); has method => ( is => 'ro', isa => 'Method', required => 1 ); has description => ( is => 'ro', isa => 'Str', predicate => 'has_description' ); +has required_payload => ( + is => 'ro', + isa => 'Boolean', + predicate => 'payload_is_required', + lazy => 1, + default => 0, + coerce => 1, +); has authentication => ( is => 'ro', isa => 'Boolean', predicate => 'has_authentication', default => 0, - coerce => 1, + coerce => 1, ); has base_url => ( is => 'ro', @@ -82,17 +90,24 @@ has expected_status => ( handles => { find_expected_status => 'grep', }, ); has optional_params => ( - traits => ['Array'], - is => 'ro', - isa => ArrayRef [Str], - predicate => 'has_optional_params', + traits => ['Array'], + is => 'ro', + isa => ArrayRef [Str], + predicate => 'has_optional_params', auto_deref => 1, ); has required_params => ( - traits => ['Array'], - is => 'ro', - isa => ArrayRef [Str], - predicate => 'has_required_params', + traits => ['Array'], + is => 'ro', + isa => ArrayRef [Str], + predicate => 'has_required_params', + auto_deref => 1, +); +has form_data => ( + traits => ['Hash'], + is => 'ro', + isa => 'HashRef', + predicate => 'has_form_data', auto_deref => 1, ); has documentation => ( @@ -128,6 +143,24 @@ sub wrap { ? delete $method_args{spore_payload} : delete $method_args{payload}; + if ( $payload + && ( $method->method ne 'POST' || $method->method ne 'PUT' ) ) + { + die Net::HTTP::Spore::Response->new( 599, [], + { error => "payload requires a PUT or POST method" }, + ); + } + + if ( $method->payload_is_required && !$payload ) { + die Net::HTTP::Spore::Response->new( + 599, + [], + { + error => "this method require a payload, and no payload is provided", + } + ); + } + if ($method->has_required_params) { foreach my $required ( $method->required_params ) { if ( !grep { $required eq $_ } keys %method_args ) { diff --git a/lib/Net/HTTP/Spore/Meta/Method/Spore.pm b/lib/Net/HTTP/Spore/Meta/Method/Spore.pm index 1cea574..a616149 100644 --- a/lib/Net/HTTP/Spore/Meta/Method/Spore.pm +++ b/lib/Net/HTTP/Spore/Meta/Method/Spore.pm @@ -54,6 +54,12 @@ sub add_spore_method { my $code = delete $options{code}; + $meta->_trace_msg( '-> attach ' + . $name . ' (' + . $options{method} . ' => ' + . $options{path} + . ')' ); + $meta->add_method( $name, Net::HTTP::Spore::Meta::Method->wrap( diff --git a/lib/Net/HTTP/Spore/Role/Debug.pm b/lib/Net/HTTP/Spore/Role/Debug.pm new file mode 100644 index 0000000..772373a --- /dev/null +++ b/lib/Net/HTTP/Spore/Role/Debug.pm @@ -0,0 +1,14 @@ +package Net::HTTP::Spore::Role::Debug; + +use Moose::Role; + +has trace => ( + is => 'rw', + isa => 'Bool', + lazy => 1, + default => sub { $ENV{SPORE_TRACE} ? 1 : 0; } +); + +sub _trace_msg { print STDOUT $_[1]."\n" if $_[0]->trace; } + +1; diff --git a/lib/Net/HTTP/Spore/Role/Description.pm b/lib/Net/HTTP/Spore/Role/Description.pm index 213955d..2723fef 100644 --- a/lib/Net/HTTP/Spore/Role/Description.pm +++ b/lib/Net/HTTP/Spore/Role/Description.pm @@ -24,4 +24,11 @@ has authentication => ( predicate => 'has_authentication', ); +has expected_status => ( + is => 'rw', + isa => 'Array', + lazy => 1, + default => sub { [] }, +); + 1; diff --git a/lib/Net/HTTP/Spore/Role/Request.pm b/lib/Net/HTTP/Spore/Role/Request.pm index d22a721..a0d61b1 100644 --- a/lib/Net/HTTP/Spore/Role/Request.pm +++ b/lib/Net/HTTP/Spore/Role/Request.pm @@ -66,6 +66,7 @@ sub _request { $result->headers, $result->content, ); + return $response; } diff --git a/t/specs/api2.json b/t/specs/api2.json new file mode 100644 index 0000000..7341405 --- /dev/null +++ b/t/specs/api2.json @@ -0,0 +1,20 @@ +{ + "name": "Test API", + "methods" : { + "get_projects" : { + "authentication": true, + "path" : "/projects/show", + "method" : "GET", + "optional_params" : [ + "name" + ] + }, + "get_project_info" : { + "required_params" : [ + "project" + ], + "path" : "/project/show", + "method" : "GET" + } + } +} diff --git a/t/spore-method/payload.t b/t/spore-method/payload.t new file mode 100644 index 0000000..483d9cc --- /dev/null +++ b/t/spore-method/payload.t @@ -0,0 +1,33 @@ +use strict; +use warnings; +use Test::More tests => 4; + +use Test::Exception; + +use JSON; +use Net::HTTP::Spore; + +my $api_with_payload = { + base_url => 'foo', + methods => { + create_user => { + method => 'POST', + path => '/user', + required_payload => 1, + }, + list_user => { + method => 'GET', + path => '/user', + } + }, +}; + +my $obj = + Net::HTTP::Spore->new_from_string( JSON::encode_json($api_with_payload), + base_url => 'http://localhost' ); + +dies_ok { $obj->create_user(); }; +like $@->body->{error}, qr/this method require a payload/; + +dies_ok { $obj->list_user( payload => {} ) }; +like $@->body->{error}, qr/payload requires a PUT or POST method/; diff --git a/t/spore/01_new_from_string.t b/t/spore/01_new_from_string.t index e9d9357..6a21994 100644 --- a/t/spore/01_new_from_string.t +++ b/t/spore/01_new_from_string.t @@ -3,18 +3,39 @@ use warnings; use Test::More; use Test::Exception; -plan tests => 14; +plan tests => 27; +use JSON; use IO::All; use Net::HTTP::Spore; my $api_spec = 't/specs/api.json'; +my $api2_spec = 't/specs/api2.json'; + my %args = ( base_url => 'http://localhost/', ); my $github_spec = "http://github.com/franckcuny/spore/raw/master/services/github.json"; -my $content < io($api_spec); +my $api_ok = { + base_url => "http://services.org/restapi", + methods => { get_info => { method => 'GET', path => '/show' } }, +}; + +my $second_api = { + base_url => "http://services.org/restapi", + methods => { list_users => { method => 'GET', path => '/users' } }, +}; + +my $api_without_path = { + base_url => "http://services.org/restapi", + methods => { get_info => { method => 'GET' } }, +}; + +my $api_without_method = { + base_url => "http://services.org/restapi", + methods => { get_info => { method => 'PET', path => '/show' } }, +}; dies_ok { Net::HTTP::Spore->new_from_spec }; like $@, qr/specification file is missing/; @@ -22,31 +43,50 @@ like $@, qr/specification file is missing/; dies_ok { Net::HTTP::Spore->new_from_spec( "/foo/bar/baz", ) }; like $@, qr/does not exists/; -dies_ok { Net::HTTP::Spore->new_from_spec( $api_spec, ) }; +dies_ok { Net::HTTP::Spore->new_from_spec( $api_spec ) }; like $@, qr/base_url is missing/; ok my $client = Net::HTTP::Spore->new_from_spec( $api_spec, %args ); -ok $client = Net::HTTP::Spore->new_from_string( $content, %args ); +ok $client = + Net::HTTP::Spore->new_from_string( JSON::encode_json($api_ok), %args ); +ok $client->meta->_find_spore_method_by_name(sub{/^get_info$/}); SKIP: { - skip "require RUN_HTTP_TEST", 1 unless $ENV{RUN_HTTP_TEST}; + skip "require RUN_HTTP_TEST", 2 unless $ENV{RUN_HTTP_TEST}; ok $client = Net::HTTP::Spore->new_from_spec( $github_spec, %args ); + ok $client->meta->_find_spore_method_by_name(sub{/^user_search$/}); } dies_ok { - Net::HTTP::Spore->new_from_string( -'{"base_url" : "http://services.org/restapi/","methods" : { "get_info" : { "method" : "GET" } } }' - ); + Net::HTTP::Spore->new_from_string( JSON::encode_json($api_without_path) ); }; like $@, qr/Attribute \(path\) is required/; dies_ok { - Net::HTTP::Spore->new_from_string( -'{"base_url" : "http://services.org/restapi/","methods" : { "get_info" : { "method" : "PET", "path":"/info" } } }' - ); + Net::HTTP::Spore->new_from_string(JSON::encode_json($api_without_method)); }; like $@, qr/Attribute \(method\) does not pass the type constraint/; -ok $client = Net::HTTP::Spore->new_from_string( -'{"base_url" : "http://services.org/restapi/","methods" : { "get_info" : { "path" : "/show", "method" : "GET" } } }' -); +ok $client = Net::HTTP::Spore->new_from_string(JSON::encode_json($api_ok)); +ok $client->meta->_find_spore_method_by_name(sub{/^get_info$/}); + +dies_ok { + Net::HTTP::Spore->new_from_strings('/a/b/c', '/a/b/c'); +}; + +for ( {}, { base_url => 'http://localhost/api' } ) { + ok $client = Net::HTTP::Spore->new_from_strings( JSON::encode_json($api_ok), + JSON::encode_json($second_api), $_ ); + ok $client->meta->_find_spore_method_by_name( sub { /^get_info$/ } ); + ok $client->meta->_find_spore_method_by_name( sub { /^list_users$/ } ); +} + +dies_ok { + $client = Net::HTTP::Spore->new_from_specs($api_spec, $api2_spec); +}; +like $@, qr/base_url is missing/; + +ok $client = + Net::HTTP::Spore->new_from_specs( $api_spec, $api2_spec, + { base_url => 'http://localhost' } ); + |
