summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfranck cuny <franck@lumberjaph.net>2010-09-13 13:31:56 +0200
committerfranck cuny <franck@lumberjaph.net>2010-09-13 13:31:56 +0200
commit3e3dc478fc9b4eb90681df89156dfcc8f7f81481 (patch)
treeb9788b0d48f524bc4c0aeeb48c744a8f7b097910
downloadnet-http-spore-3e3dc478fc9b4eb90681df89156dfcc8f7f81481.tar.gz
initial import
Diffstat (limited to '')
-rw-r--r--eg/api.pl24
-rw-r--r--eg/apitest.json13
-rw-r--r--eg/apitest.yaml12
-rw-r--r--eg/couchdb.pl38
-rw-r--r--eg/github.json36
-rw-r--r--eg/github.yaml17
-rw-r--r--eg/test.pl21
-rw-r--r--eg/twitter.json27
-rw-r--r--eg/twitter.yaml19
-rw-r--r--lib/Net/HTTP/Spore.pm68
-rw-r--r--lib/Net/HTTP/Spore/Core.pm5
-rw-r--r--lib/Net/HTTP/Spore/Meta.pm47
-rw-r--r--lib/Net/HTTP/Spore/Meta/Class.pm13
-rw-r--r--lib/Net/HTTP/Spore/Meta/Method.pm159
-rw-r--r--lib/Net/HTTP/Spore/Meta/Method/Spore.pm113
-rw-r--r--lib/Net/HTTP/Spore/Middleware.pm31
-rw-r--r--lib/Net/HTTP/Spore/Middleware/Auth/Basic.pm24
-rw-r--r--lib/Net/HTTP/Spore/Middleware/Auth/OAuth.pm37
-rw-r--r--lib/Net/HTTP/Spore/Middleware/Format.pm37
-rw-r--r--lib/Net/HTTP/Spore/Middleware/Format/Auto.pm17
-rw-r--r--lib/Net/HTTP/Spore/Middleware/Format/JSON.pm19
-rw-r--r--lib/Net/HTTP/Spore/Middleware/Format/XML.pm13
-rw-r--r--lib/Net/HTTP/Spore/Middleware/Format/YAML.pm12
-rw-r--r--lib/Net/HTTP/Spore/Middleware/LogDispatch.pm7
-rw-r--r--lib/Net/HTTP/Spore/Middleware/Runtime.pm22
-rw-r--r--lib/Net/HTTP/Spore/Middleware/Test.pm12
-rw-r--r--lib/Net/HTTP/Spore/Middleware/Test/Response.pm28
-rw-r--r--lib/Net/HTTP/Spore/Middleware/UserAgent.pm15
-rw-r--r--lib/Net/HTTP/Spore/Request.pm156
-rw-r--r--lib/Net/HTTP/Spore/Response.pm103
-rw-r--r--lib/Net/HTTP/Spore/Role/Middleware.pm44
-rw-r--r--lib/Net/HTTP/Spore/Role/Request.pm82
-rw-r--r--lib/Net/HTTP/Spore/Role/UserAgent.pm22
-rw-r--r--spec/spore.pod105
-rw-r--r--t/specs/couchdb.json77
-rw-r--r--t/spore-method/base.t38
-rw-r--r--t/spore-middleware/auth-basic.t32
-rw-r--r--t/spore-middleware/format-json.t30
-rw-r--r--t/spore-middleware/format-xml.t30
-rw-r--r--t/spore-middleware/format-yaml.t30
-rw-r--r--t/spore-middleware/runtime.t19
-rw-r--r--t/spore-middleware/useragent.t19
-rw-r--r--t/spore-request/base.t71
-rw-r--r--t/spore-request/exception.t17
-rw-r--r--t/spore-request/finalize.t27
-rw-r--r--t/spore-request/new.t25
-rw-r--r--t/spore-request/path_info.t25
-rw-r--r--t/spore-request/query_string.t25
-rw-r--r--t/spore-request/uri.t109
-rw-r--r--t/spore-response/body.t21
-rw-r--r--t/spore-response/headers.t20
-rw-r--r--t/spore-response/new.t34
-rw-r--r--t/spore-response/response.t23
53 files changed, 2070 insertions, 0 deletions
diff --git a/eg/api.pl b/eg/api.pl
new file mode 100644
index 0000000..4e89701
--- /dev/null
+++ b/eg/api.pl
@@ -0,0 +1,24 @@
+use strict;
+use warnings;
+use 5.010;
+
+use Net::HTTP::Spore;
+
+my $username = shift;
+my $token = shift;
+
+my $api = Net::HTTP::Spore->new_from_spec(shift);
+
+$api->enable('Net::HTTP::Spore::Middleware::Format::JSON');
+
+$api->enable(
+ 'Net::HTTP::Spore::Middleware::Auth::Basic',
+ username => $username,
+ password => $token,
+);
+
+my ( $content, $result ) =
+ $api->user_information( format => 'json', username => 'franckcuny' );
+
+use YAML::Syck;
+warn Dump $content;
diff --git a/eg/apitest.json b/eg/apitest.json
new file mode 100644
index 0000000..8c26bae
--- /dev/null
+++ b/eg/apitest.json
@@ -0,0 +1,13 @@
+{
+ "methods" : {
+ "new_user" : {
+ "path" : "/user/",
+ "method" : "POST"
+ }
+ },
+ "declare" : {
+ "api_base_url" : "http://localhost:5000",
+ "api_format_mode" : "content-type",
+ "api_format" : "json"
+ }
+}
diff --git a/eg/apitest.yaml b/eg/apitest.yaml
new file mode 100644
index 0000000..9e3bad4
--- /dev/null
+++ b/eg/apitest.yaml
@@ -0,0 +1,12 @@
+name: apitest
+author:
+ - franck cuny <franck@lumberjaph.net>
+version: 0.01
+api_base_url: http://localhost:5000
+methods:
+ new_user:
+ method: POST
+ path: /user/
+ list_users:
+ method: GET
+ path: /user/list
diff --git a/eg/couchdb.pl b/eg/couchdb.pl
new file mode 100644
index 0000000..737e76b
--- /dev/null
+++ b/eg/couchdb.pl
@@ -0,0 +1,38 @@
+use strict;
+use warnings;
+use 5.010;
+use YAML::Syck;
+use Net::HTTP::Spore;
+use Try::Tiny;
+
+my $api = Net::HTTP::Spore->new_from_spec(shift, api_base_url => 'http://localhost:5984');
+
+$api->enable('Format::JSON');
+$api->enable('Runtime');
+$api->enable('UserAgent');
+
+#my $documents = $api->get_all_documents(database => 'spore');
+#warn Dump $documents;
+#say "status => ".$documents->[0];
+#say "body => ".Dump $documents->[2];
+#say "headers=> ".Dump $documents->[1];
+
+my $res;
+
+#$res = $api->create_document_with_id(database => 'spore', doc_id => 1, payload => {foo => 'bar'});
+#warn Dump $res;
+
+#$res = $api->delete_document(database =>'spore', doc_id => 1, rev => $res->body->{rev});
+#warn Dump $res;
+
+$res = $api->create_document_without_id(database => 'spore', payload => {foo => 'baz', bar => 'foobaz'});
+warn Dump $res;
+
+#try {
+ #$res = $api->get_document( database => 'spore', doc_id => 1 );
+#}
+#catch {
+ #warn Dump $_->[2];
+ #warn Dump $_->[1];
+#};
+
diff --git a/eg/github.json b/eg/github.json
new file mode 100644
index 0000000..4e3a051
--- /dev/null
+++ b/eg/github.json
@@ -0,0 +1,36 @@
+{
+ "declare" : {
+ "api_base_url" : "http://github.com/api/v2/",
+ "api_format_mode" : "content-type",
+ "api_format" : "json"
+ },
+ "methods" : {
+ "user_information" : {
+ "params" : [
+ "username",
+ "format"
+ ],
+ "required" : [
+ "format",
+ "username"
+ ],
+ "path" : "/:format/user/show/:username",
+ "method" : "GET",
+ "expected" : [
+ "200"
+ ]
+ },
+ "user_following" : {
+ "params" : [
+ "user",
+ "format"
+ ],
+ "required" : [
+ "user",
+ "format"
+ ],
+ "path" : "/:format/user/show/:user/followers",
+ "method" : "GET"
+ }
+ }
+}
diff --git a/eg/github.yaml b/eg/github.yaml
new file mode 100644
index 0000000..f844a41
--- /dev/null
+++ b/eg/github.yaml
@@ -0,0 +1,17 @@
+declare:
+ api_base_url: http://github.com/api/v2/
+methods:
+ user_information:
+ method: GET
+ path: /user/show/:username
+ params:
+ - username
+ required:
+ - username
+ user_following:
+ method: GET
+ path: /user/show/:user/followers
+ params:
+ - user
+ required:
+ - user
diff --git a/eg/test.pl b/eg/test.pl
new file mode 100644
index 0000000..b77d0bb
--- /dev/null
+++ b/eg/test.pl
@@ -0,0 +1,21 @@
+use strict;
+use warnings;
+use 5.010;
+
+use Net::HTTP::Spore;
+
+my $api = Net::HTTP::Spore->new_from_spec(shift, api_base_url => 'http://localhost:5000');
+
+$api->enable('Net::HTTP::Spore::Middleware::Format::JSON');
+
+$api->enable(
+ 'Net::HTTP::Spore::Middleware::Auth::Basic',
+ username => 'admin',
+ password => 's3cr3t'
+);
+
+my $content =
+ $api->new_user( input => { user => { francktest => { name => 'franck' } } } );
+
+use YAML::Syck;
+warn Dump $content;
diff --git a/eg/twitter.json b/eg/twitter.json
new file mode 100644
index 0000000..f07470e
--- /dev/null
+++ b/eg/twitter.json
@@ -0,0 +1,27 @@
+{
+ "declare" : {
+ "api_base_url" : "http://api.twitter.com/1",
+ "api_format_mode" : "append",
+ "api_format" : "json"
+ },
+ "methods" : {
+ "public_timeline" : {
+ "params" : [
+ "skip_user"
+ ],
+ "path" : "/statuses/public_timeline",
+ "method" : "GET"
+ },
+ "home_timeline" : {
+ "params" : [
+ "since_id",
+ "max_id",
+ "count",
+ "page",
+ "skip_user"
+ ],
+ "path" : "/statuses/home_timeline",
+ "method" : "GET"
+ }
+ }
+}
diff --git a/eg/twitter.yaml b/eg/twitter.yaml
new file mode 100644
index 0000000..92054b7
--- /dev/null
+++ b/eg/twitter.yaml
@@ -0,0 +1,19 @@
+declare:
+ api_base_url: http://api.twitter.com/1
+ api_format: json
+ api_format_mode: append
+methods:
+ public_timeline:
+ method: GET
+ path: /statuses/public_timeline
+ params:
+ - skip_user
+ home_timeline:
+ method: GET
+ path: /statuses/home_timeline
+ params:
+ - since_id
+ - max_id
+ - count
+ - page
+ - skip_user
diff --git a/lib/Net/HTTP/Spore.pm b/lib/Net/HTTP/Spore.pm
new file mode 100644
index 0000000..88ce418
--- /dev/null
+++ b/lib/Net/HTTP/Spore.pm
@@ -0,0 +1,68 @@
+package Net::HTTP::Spore;
+
+use Moose;
+
+use IO::All;
+use JSON;
+use Carp;
+use Try::Tiny;
+
+use Net::HTTP::Spore::Core;
+
+our $VERSION = 0.01;
+
+sub new_from_spec {
+ my ($class, $spec_file, %args) = @_;
+
+ if (! -f $spec_file) {
+ Carp::confess ("$spec_file does not exists");
+ }
+
+ my ($content, $spec);
+
+ $content < io($spec_file);
+
+ try {
+ $spec = JSON::decode_json($content);
+ }
+ catch {
+ Carp::confess( "unable to parse JSON spec: " . $_ );
+ };
+
+ my $spore_class =
+ Class::MOP::Class->create_anon_class(
+ superclasses => ['Net::HTTP::Spore::Core']);
+
+ my $spore_object;
+ try {
+
+ my $api_base_url;
+ if ( $spec->{api_base_url} && !$args{api_base_url} ) {
+ $args{api_base_url} = $spec->{api_base_url};
+ }
+ elsif ( !$args{api_base_url} ) {
+ die "api_base_url is missing!";
+ }
+
+ $spore_object = $spore_class->new_object(%args);
+ $spore_object = _add_methods($spore_object, $spec->{methods});
+
+ }catch{
+ Carp::confess("unable to create new Net::HTTP::Spore object: ".$_);
+ };
+
+ return $spore_object;
+}
+
+sub _add_methods {
+ my ($class, $methods_spec) = @_;
+
+ foreach my $method_name (keys %$methods_spec) {
+ $class->meta->add_spore_method($method_name,
+ %{$methods_spec->{$method_name}});
+ }
+ $class;
+}
+
+
+1;
diff --git a/lib/Net/HTTP/Spore/Core.pm b/lib/Net/HTTP/Spore/Core.pm
new file mode 100644
index 0000000..2251af8
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Core.pm
@@ -0,0 +1,5 @@
+package Net::HTTP::Spore::Core;
+
+use Net::HTTP::Spore::Meta;
+
+1;
diff --git a/lib/Net/HTTP/Spore/Meta.pm b/lib/Net/HTTP/Spore/Meta.pm
new file mode 100644
index 0000000..8b4942a
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Meta.pm
@@ -0,0 +1,47 @@
+package Net::HTTP::Spore::Meta;
+
+use Moose;
+use Moose::Exporter;
+use Moose::Util::MetaRole;
+
+our $VERSION = '0.14';
+
+Moose::Exporter->setup_import_methods(
+ with_meta => [qw/spore_method/],
+ also => [qw/Moose/]
+);
+
+sub spore_method {
+ my $meta = shift;
+ my $name = shift;
+ $meta->add_spore_method($name, @_);
+}
+
+sub init_meta {
+ my ($class, %options) = @_;
+
+ my $for = $options{for_class};
+ Moose->init_meta(%options);
+
+ my $meta = Moose::Util::MetaRole::apply_metaroles(
+ for => $for,
+ class_metaroles => {
+ class => ['Net::HTTP::Spore::Meta::Class'],
+ },
+ );
+
+ Moose::Util::MetaRole::apply_base_class_roles(
+ for => $for,
+ roles => [
+ qw/
+ Net::HTTP::Spore::Role::UserAgent
+ Net::HTTP::Spore::Role::Request
+ Net::HTTP::Spore::Role::Middleware
+ /
+ ],
+ );
+
+ $meta;
+};
+
+1;
diff --git a/lib/Net/HTTP/Spore/Meta/Class.pm b/lib/Net/HTTP/Spore/Meta/Class.pm
new file mode 100644
index 0000000..4ddd5c6
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Meta/Class.pm
@@ -0,0 +1,13 @@
+package Net::HTTP::Spore::Meta::Class;
+
+# ABSTRACT: metaclass for all API client
+
+use Moose::Role;
+
+with qw/Net::HTTP::Spore::Meta::Method::Spore/;
+
+1;
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
diff --git a/lib/Net/HTTP/Spore/Meta/Method.pm b/lib/Net/HTTP/Spore/Meta/Method.pm
new file mode 100644
index 0000000..0087147
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Meta/Method.pm
@@ -0,0 +1,159 @@
+package Net::HTTP::Spore::Meta::Method;
+
+# ABSTRACT: create api method
+
+use Moose;
+use Moose::Util::TypeConstraints;
+
+use MooseX::Types::Moose qw/Str Int ArrayRef/;
+use MooseX::Types::URI qw/Uri/;
+
+extends 'Moose::Meta::Method';
+use Net::HTTP::Spore::Response;
+
+subtype UriPath
+ => as 'Str'
+ => where { $_ =~ m!^/! }
+ => message {"path must start with /"};
+
+enum Method => qw(HEAD GET POST PUT DELETE);
+
+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 authentication => (
+ is => 'ro',
+ isa => 'Bool',
+ predicate => 'has_authentication',
+ default => 0
+);
+has api_base_url => (
+ is => 'ro',
+ isa => Uri,
+ coerce => 1,
+ predicate => 'has_api_base_url',
+);
+has expected => (
+ traits => ['Array'],
+ is => 'ro',
+ isa => ArrayRef [Int],
+ auto_deref => 1,
+ required => 0,
+ predicate => 'has_expected',
+ handles => {find_expected_code => 'grep',},
+);
+has params => (
+ traits => ['Array'],
+ is => 'ro',
+ isa => ArrayRef [Str],
+ required => 0,
+ default => sub { [] },
+ auto_deref => 1,
+ handles => {find_request_parameter => 'first',}
+);
+has required => (
+ traits => ['Array'],
+ is => 'ro',
+ isa => ArrayRef [Str],
+ default => sub { [] },
+ auto_deref => 1,
+ required => 0,
+);
+has documentation => (
+ is => 'ro',
+ isa => 'Str',
+ lazy => 1,
+ default => sub {
+ my $self = shift;
+ my $doc;
+ $doc .= "name: " . $self->name . "\n";
+ $doc .= "description: " . $self->description . "\n"
+ if $self->has_description;
+ $doc .= "method: " . $self->method . "\n";
+ $doc .= "path: " . $self->path . "\n";
+ $doc .= "arguments: " . join(', ', $self->params) . "\n"
+ if $self->params;
+ $doc .= "required: " . join(', ', $self->required) . "\n"
+ if $self->required;
+ $doc;
+ }
+);
+
+sub wrap {
+ my ( $class, %args ) = @_;
+
+ my $code = sub {
+ my ( $self, %method_args ) = @_;
+
+ my $method = $self->meta->find_spore_method_by_name( $args{name} );
+
+ my $payload =
+ ( defined $method_args{spore_payload} )
+ ? delete $method_args{spore_payload}
+ : delete $method_args{payload};
+
+ foreach my $required ( $method->required ) {
+ if ( !grep { $required eq $_ } keys %method_args ) {
+ die Net::HTTP::Spore::Response->new(
+ 599,
+ [],
+ {
+ error =>
+ "$required is marked as required but is missing",
+ }
+ );
+ }
+ }
+
+ my $params;
+ foreach (keys %method_args) {
+ push @$params, $_, $method_args{$_};
+ }
+
+ my $api_base_url =
+ $method->has_api_base_url
+ ? $method->api_base_url
+ : $self->api_base_url;
+
+ my $env = {
+ REQUEST_METHOD => $method->method,
+ SERVER_NAME => $api_base_url->host,
+ SERVER_PORT => $api_base_url->port,
+ SCRIPT_NAME => (
+ $api_base_url->path eq '/'
+ ? ''
+ : $api_base_url->path
+ ),
+ PATH_INFO => $method->path,
+ REQUEST_URI => '',
+ QUERY_STRING => '',
+ SERVER_PROTOCOL => $api_base_url->scheme,
+ HTTP_USER_AGENT => $self->api_useragent->agent,
+ 'spore.expected' => [ $method->expected ],
+ 'spore.authentication' => $method->authentication,
+ 'spore.params' => $params,
+ 'spore.payload' => $payload,
+ 'spore.errors' => *STDERR,
+ 'spore.url_scheme' => $api_base_url->scheme,
+ };
+
+ my $response = $self->http_request($env);
+ my $code = $response->status;
+
+ die $response if ( $method->has_expected
+ && !$method->find_expected_code( sub { /$code/ } ) );
+
+ $response;
+ };
+ $args{body} = $code;
+
+ $class->SUPER::wrap(%args);
+}
+
+1;
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
diff --git a/lib/Net/HTTP/Spore/Meta/Method/Spore.pm b/lib/Net/HTTP/Spore/Meta/Method/Spore.pm
new file mode 100644
index 0000000..4c6fe71
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Meta/Method/Spore.pm
@@ -0,0 +1,113 @@
+package Net::HTTP::Spore::Meta::Method::Spore;
+
+# ABSTRACT: declare API method
+
+use Moose::Role;
+use Net::HTTP::API::Error;
+use Net::HTTP::Spore::Meta::Method;
+use MooseX::Types::Moose qw/Str ArrayRef/;
+
+has local_spore_methods => (
+ traits => ['Array'],
+ is => 'rw',
+ isa => ArrayRef [Str],
+ required => 1,
+ default => sub { [] },
+ auto_deref => 1,
+ handles => {
+ _find_spore_method_by_name => 'first',
+ _add_spore_method => 'push',
+ get_all_spore_methods => 'elements',
+ },
+);
+
+sub find_spore_method_by_name {
+ my ($meta, $name) = @_;
+ my $method_name = $meta->_find_spore_method_by_name(sub {/^$name$/});
+ return unless $method_name;
+ my $method = $meta->find_method_by_name($method_name);
+ if ($method->isa('Class::MOP::Method::Wrapped')) {
+ return $method->get_original_method;
+ }
+ else {
+ return $method;
+ }
+}
+
+sub remove_spore_method {
+ my ($meta, $name) = @_;
+ my @methods = grep { !/$name/ } $meta->get_all_spore_methods;
+ $meta->local_spore_methods(\@methods);
+ $meta->remove_method($name);
+}
+
+before add_spore_method => sub {
+ my ($meta, $name) = @_;
+ if ($meta->_find_spore_method_by_name(sub {/^$name$/})) {
+ die Net::HTTP::API::Error->new(
+ reason => "method '$name' is already declared in " . $meta->name);
+ }
+};
+
+sub add_spore_method {
+ my ($meta, $name, %options) = @_;
+
+ my $code = delete $options{code};
+
+ $meta->add_method(
+ $name,
+ Net::HTTP::Spore::Meta::Method->wrap(
+ name => $name,
+ package_name => $meta->name,
+ body => $code,
+ %options
+ ),
+ );
+ $meta->_add_spore_method($name);
+}
+
+after add_spore_method => sub {
+ my ($meta, $name) = @_;
+ $meta->add_before_method_modifier(
+ $name,
+ sub {
+ my $self = shift;
+ die Net::HTTP::API::Error->new(
+ reason => "'api_base_url' have not been defined")
+ unless $self->api_base_url;
+ }
+ );
+};
+
+1;
+
+=head1 SYNOPSIS
+
+ my $api_client = MyAPI->new;
+
+ my @methods = $api_client->meta->get_all_api_methods();
+
+ my $method = $api_client->meta->find_spore_method_by_name('users');
+
+ $api_client->meta->remove_spore_method($method);
+
+ $api_client->meta->add_spore_method('users', sub {...},
+ description => 'this method does...',);
+
+=head1 DESCRIPTION
+
+=method get_all_spore_methods
+
+Return a list of net api methods
+
+=method find_spore_method_by_name
+
+Return a net api method
+
+=method remove_spore_method
+
+Remove a net api method
+
+=method add_spore_method
+
+Add a net api method
diff --git a/lib/Net/HTTP/Spore/Middleware.pm b/lib/Net/HTTP/Spore/Middleware.pm
new file mode 100644
index 0000000..0b8584c
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Middleware.pm
@@ -0,0 +1,31 @@
+package Net::HTTP::Spore::Middleware;
+
+use strict;
+use warnings;
+
+sub new {
+ my $class = shift;
+ bless {@_}, $class;
+}
+
+sub response_cb {
+ my ($self, $cb) = @_;
+
+ my $body_filter = sub {
+ my $filter = $cb->(@_);
+ };
+ return $body_filter;
+}
+
+sub wrap {
+ my ($self, @args) = @_;
+
+ if (!ref $self) {
+ $self = $self->new(@args);
+ }
+ return sub {
+ $self->call(@_);
+ };
+}
+
+1;
diff --git a/lib/Net/HTTP/Spore/Middleware/Auth/Basic.pm b/lib/Net/HTTP/Spore/Middleware/Auth/Basic.pm
new file mode 100644
index 0000000..18c1e16
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Middleware/Auth/Basic.pm
@@ -0,0 +1,24 @@
+package Net::HTTP::Spore::Middleware::Auth::Basic;
+
+use Moose;
+use MIME::Base64;
+
+extends 'Net::HTTP::Spore::Middleware';
+
+has username => (isa => 'Str', is => 'rw', predicate => 'has_username');
+has password => (isa => 'Str', is => 'rw', predicate => 'has_password');
+
+sub call {
+ my ( $self, $req ) = @_;
+
+ if ( $self->has_username && $self->has_password ) {
+ $req->header(
+ 'Authorization' => 'Basic '
+ . MIME::Base64::encode(
+ $self->username . ':' . $self->password, ''
+ )
+ );
+ }
+}
+
+1;
diff --git a/lib/Net/HTTP/Spore/Middleware/Auth/OAuth.pm b/lib/Net/HTTP/Spore/Middleware/Auth/OAuth.pm
new file mode 100644
index 0000000..e30a45b
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Middleware/Auth/OAuth.pm
@@ -0,0 +1,37 @@
+package Net::HTTP::Spore::Middleware::Auth::OAuth;
+
+use Moose;
+extends 'Net::HTTP::Spore::Middleware';
+
+use Net::OAuth;
+use MIME::Base64;
+
+has [qw/consumer_key consumer_secret token token_secret/] => (
+ is => 'ro',
+ isa => 'Str',
+ required => 1,
+);
+
+sub call {
+ my ( $self, $req ) = @_;
+
+ my $request = Net::OAuth->request('protected resource')->new(
+ version => '1.0',
+ consumer_key => $self->consumer_key,
+ consumer_secret => $self->consumer_secret,
+ token => $self->token,
+ token_secret => $self->token_secret,
+ request_method => $req->method,
+ signature_method => 'HMAC-SHA1',
+ timestamp => time,
+ nonce => MIME::Base64::encode( time . $$ . rand ),
+ request_url => $req->uri,
+ # extra_params => \%post_args,
+ );
+
+ $request->sign;
+ my $auth = $request->to_authorization_header;
+ $req->header( 'Authorization' => $auth );
+}
+
+1;
diff --git a/lib/Net/HTTP/Spore/Middleware/Format.pm b/lib/Net/HTTP/Spore/Middleware/Format.pm
new file mode 100644
index 0000000..7acd376
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Middleware/Format.pm
@@ -0,0 +1,37 @@
+package Net::HTTP::Spore::Middleware::Format;
+
+use Moose;
+extends 'Net::HTTP::Spore::Middleware';
+
+sub encode { die "must be implemented" }
+sub decode { die "must be implemented" }
+sub accept_type { die "must be implemented" }
+sub content_type { die "must be implemented" }
+
+sub call {
+ my ( $self, $req ) = @_;
+
+ return
+ if ( exists $req->env->{'sporex.format'}
+ && $req->env->{'sporex.format'} == 1 );
+
+ $req->header( $self->accept_type );
+
+ if ( $req->env->{'spore.payload'} ) {
+ $req->env->{'spore.payload'} =
+ $self->encode( $req->env->{'spore.payload'} );
+ $req->header( $self->content_type );
+ }
+
+ $req->env->{'sporex.format'} = 1;
+
+ return $self->response_cb(
+ sub {
+ my $res = shift;
+ my $content = $self->decode( $res->body );
+ $res->body($content);
+ }
+ );
+}
+
+1;
diff --git a/lib/Net/HTTP/Spore/Middleware/Format/Auto.pm b/lib/Net/HTTP/Spore/Middleware/Format/Auto.pm
new file mode 100644
index 0000000..fd66b8c
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Middleware/Format/Auto.pm
@@ -0,0 +1,17 @@
+package Net::HTTP::Spore::Middleware::Format::Auto;
+
+use Moose;
+extends 'Net::HTTP::Spore::Middleware::Format';
+
+sub call {
+ my ( $self, $req ) = @_;
+
+ $req->env->{'sporex.format'} = 1;
+
+ return $self->response_cb( sub {
+ my $res = shift;
+ return $res;
+ });
+}
+
+1;
diff --git a/lib/Net/HTTP/Spore/Middleware/Format/JSON.pm b/lib/Net/HTTP/Spore/Middleware/Format/JSON.pm
new file mode 100644
index 0000000..61326cd
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Middleware/Format/JSON.pm
@@ -0,0 +1,19 @@
+package Net::HTTP::Spore::Middleware::Format::JSON;
+
+use JSON;
+use Moose;
+extends 'Net::HTTP::Spore::Middleware::Format';
+
+has _json_parser => (
+ is => 'rw',
+ isa => 'JSON',
+ lazy => 1,
+ default => sub { JSON->new->allow_nonref },
+);
+
+sub encode { $_[0]->_json_parser->encode( $_[1] ); }
+sub decode { $_[0]->_json_parser->decode( $_[1] ); }
+sub accept_type { ( 'Accept' => 'application/json' ) }
+sub content_type { ( 'Content-Type' => 'application/json' ) }
+
+1;
diff --git a/lib/Net/HTTP/Spore/Middleware/Format/XML.pm b/lib/Net/HTTP/Spore/Middleware/Format/XML.pm
new file mode 100644
index 0000000..c4ae038
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Middleware/Format/XML.pm
@@ -0,0 +1,13 @@
+package Net::HTTP::Spore::Middleware::Format::XML;
+
+use Moose;
+extends 'Net::HTTP::Spore::Middleware::Format';
+
+use XML::Simple;
+
+sub accept_type { ( 'Accept' => 'text/xml' ); }
+sub content_type { ( 'Content-Type' => 'text/xml' ) }
+sub encode { XMLout( $_[1] ) }
+sub decode { XMLin( $_[1] ) }
+
+1;
diff --git a/lib/Net/HTTP/Spore/Middleware/Format/YAML.pm b/lib/Net/HTTP/Spore/Middleware/Format/YAML.pm
new file mode 100644
index 0000000..bd844ce
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Middleware/Format/YAML.pm
@@ -0,0 +1,12 @@
+package Net::HTTP::Spore::Middleware::Format::YAML;
+
+use YAML;
+use Moose;
+extends 'Net::HTTP::Spore::Middleware::Format';
+
+sub encode { YAML::Decode( $_[1] ); }
+sub decode { YAML::Load( $_[1] ); }
+sub accept_type { ( 'Accept' => 'text/x-yaml' ) }
+sub content_type { ( 'Content-Type' => 'text/x-yaml' ) }
+
+1;
diff --git a/lib/Net/HTTP/Spore/Middleware/LogDispatch.pm b/lib/Net/HTTP/Spore/Middleware/LogDispatch.pm
new file mode 100644
index 0000000..2724fcf
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Middleware/LogDispatch.pm
@@ -0,0 +1,7 @@
+package Net::HTTP::Spore::Middleware::LogDispatch;
+
+use Moose;
+extends 'Net::HTTP::Spore::Middleware';
+
+
+1;
diff --git a/lib/Net/HTTP/Spore/Middleware/Runtime.pm b/lib/Net/HTTP/Spore/Middleware/Runtime.pm
new file mode 100644
index 0000000..1614c31
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Middleware/Runtime.pm
@@ -0,0 +1,22 @@
+package Net::HTTP::Spore::Middleware::Runtime;
+
+use Moose;
+extends 'Net::HTTP::Spore::Middleware';
+use Time::HiRes;
+
+sub call {
+ my ( $self, $req) = @_;
+
+ my $start_time = [Time::HiRes::gettimeofday];
+
+ $self->response_cb(
+ sub {
+ my $res = shift;
+ my $req_time = sprintf '%.6f',
+ Time::HiRes::tv_interval($start_time);
+ $res->header('X-Spore-Runtime' => $req_time);
+ }
+ );
+}
+
+1;
diff --git a/lib/Net/HTTP/Spore/Middleware/Test.pm b/lib/Net/HTTP/Spore/Middleware/Test.pm
new file mode 100644
index 0000000..6cf2c9e
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Middleware/Test.pm
@@ -0,0 +1,12 @@
+package Net::HTTP::Spore::Middleware::Test;
+
+use strict;
+use warnings;
+
+use parent qw/Net::HTTP::Spore::Middleware/;
+
+sub call {
+# use YAML::Syck; warn Dump \@_;
+}
+
+1;
diff --git a/lib/Net/HTTP/Spore/Middleware/Test/Response.pm b/lib/Net/HTTP/Spore/Middleware/Test/Response.pm
new file mode 100644
index 0000000..ca216c5
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Middleware/Test/Response.pm
@@ -0,0 +1,28 @@
+package Net::HTTP::Spore::Middleware::Test::Response;
+
+use Moose;
+extends 'Net::HTTP::Spore::Middleware';
+
+has status => ( isa => 'Int', is => 'ro', lazy => 1, default => 200 );
+has headers => ( isa => 'ArrayRef', is => 'ro', default => sub { [] } );
+has callback => (
+ isa => 'CodeRef',
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ sub {
+ my ( $self, $req ) = @_;
+ $req->new_response( $self->status, $self->headers, $self->body, );
+ }
+ }
+);
+
+has body =>
+ ( isa => 'HashRef', is => 'ro', lazy => 1, default => sub { { foo => 1 } } );
+
+sub call {
+ my ( $self, $req ) = @_;
+ $self->callback->($self, $req);
+}
+
+1;
diff --git a/lib/Net/HTTP/Spore/Middleware/UserAgent.pm b/lib/Net/HTTP/Spore/Middleware/UserAgent.pm
new file mode 100644
index 0000000..0517c26
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Middleware/UserAgent.pm
@@ -0,0 +1,15 @@
+package Net::HTTP::Spore::Middleware::UserAgent;
+
+use Moose;
+extends qw/Net::HTTP::Spore::Middleware/;
+
+has useragent => (is => 'ro', isa => 'Str', required => 1);
+
+sub call {
+ my ($self, $req) = @_;
+
+ $req->header('User-Agent' => $self->useragent);
+}
+
+
+1;
diff --git a/lib/Net/HTTP/Spore/Request.pm b/lib/Net/HTTP/Spore/Request.pm
new file mode 100644
index 0000000..655f128
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Request.pm
@@ -0,0 +1,156 @@
+package Net::HTTP::Spore::Request;
+
+use strict;
+use warnings;
+
+use URI;
+use HTTP::Headers;
+use HTTP::Request;
+use URI::Escape;
+use Hash::MultiValue;
+
+use Net::HTTP::Spore::Response;
+
+sub new {
+ my ( $class, $env ) = @_;
+ bless { env => $env }, $class;
+}
+
+sub env { $_[0]->{env}; }
+sub method { $_[0]->{env}->{REQUEST_METHOD} }
+sub port { $_[0]->{env}->{SERVER_PORT} }
+sub script_name { $_[0]->{env}->{SCRIPT_NAME} }
+sub path { $_[0]->{env}->{PATH_INFO} || '/' }
+sub request_uri { $_[0]->{env}->{REQUEST_URI} }
+sub protocol { $_[0]->{env}->{SERVER_PROTOCOL} }
+sub content { $_[0]->{env}->{'spore.payload'} }
+sub scheme { $_[0]->{env}->{'spore.scheme'} }
+
+sub path_info {
+ my $self = shift;
+ my ($path) = $self->_path;
+ $path;
+}
+
+sub _path {
+ my $self = shift;
+
+ my $query_string;
+ my $path = $self->env->{PATH_INFO};
+ my @params = @{ $self->env->{'spore.params'} || [] };
+
+
+ my $j = 0;
+ for (my $i = 0; $i < scalar @params; $i++) {
+ my $key = $params[$i];
+ my $value = $params[++$i];
+ if (!$value) {
+ $query_string .= $key;
+ last;
+ }
+ unless ( $path && $path =~ s/\:$key/$value/ ) {
+ $query_string .= $key . '=' . $value;
+ $query_string .= '&' if $query_string && scalar @params;
+ }
+ }
+
+ $query_string =~ s/&$// if $query_string;
+ return ( $path, $query_string );
+}
+
+sub query_string {
+ my $self = shift;
+ my ( undef, $query_string ) = $self->_path;
+ $query_string;
+}
+
+sub headers {
+ my $self = shift;
+ if ( !defined $self->{headers} ) {
+ my $env = $self->env;
+ $self->{headers} = HTTP::Headers->new(
+ map {
+ ( my $field = $_ ) =~ s/^HTTPS?_//;
+ ( $field => $env->{$_} );
+ } grep { /^(?:HTTP|CONTENT)/i } keys %$env
+ );
+ }
+ $self->{headers};
+}
+
+sub header {shift->headers->header(@_)}
+
+sub uri {
+ my $self = shift;
+
+ my $path_info = shift;
+ my $query_string = shift;
+
+ if ( !$path_info || !$query_string ) {
+ my @path_info = $self->_path;
+ $path_info = $path_info[0] if !$path_info;
+ $query_string = $path_info[1] if !$query_string;
+ }
+
+ my $base = $self->_uri_base;
+
+ my $path_escape_class = '^A-Za-z0-9\-\._~/';
+
+ my $path = URI::Escape::uri_escape($path_info || '', $path_escape_class);
+
+ if (defined $query_string) {
+ $path .= '?' . $query_string;
+ }
+
+ $base =~ s!/$!! if $path =~ m!^/!;
+ return URI->new( $base . $path )->canonical;
+}
+
+sub query_parameters {
+ my $self = shift;
+}
+
+sub base {
+ my $self = shift;
+ URI->new( $self->_uri_base )->canonical;
+}
+
+sub _uri_base {
+ my $self = shift;
+ my $env = $self->env;
+
+ my $uri =
+ ( $env->{'spore.url_scheme'} || "http" ) . "://"
+ . (
+ $env->{HTTP_HOST}
+ || (( $env->{SERVER_NAME} || "" ) . ":"
+ . ( $env->{SERVER_PORT} || 80 ) )
+ ) . ( $env->{SCRIPT_NAME} || '/' );
+ return $uri;
+}
+
+sub new_response {
+ my $self = shift;
+ my $res = Net::HTTP::Spore::Response->new(@_);
+ $res->request($self);
+ $res;
+}
+
+sub finalize {
+ my $self = shift;
+
+ my ($path_info, $query_string) = $self->_path;
+
+ $self->env->{PATH_INFO} = $path_info;
+ $self->env->{QUERY_STRING} = $query_string || '';
+
+ my $uri = $self->uri($path_info, $query_string);
+
+ my $request =
+ HTTP::Request->new( $self->method => $uri, $self->headers );
+
+ $request->content($self->content) if ($self->content);
+ $request;
+}
+
+1;
diff --git a/lib/Net/HTTP/Spore/Response.pm b/lib/Net/HTTP/Spore/Response.pm
new file mode 100644
index 0000000..d695dfa
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Response.pm
@@ -0,0 +1,103 @@
+package Net::HTTP::Spore::Response;
+
+use strict;
+use warnings;
+
+use overload '@{}' => \&finalize;
+
+use HTTP::Headers;
+
+sub new {
+ my ( $class, $rc, $headers, $body ) = @_;
+
+ my $self = bless {}, $class;
+ $self->status($rc) if defined $rc;
+ if (defined $body) {
+ $self->body($body);
+ $self->raw_body($body);
+ }
+ $self->headers($headers || []);
+ $self;
+}
+
+sub code { shift->status(@_) }
+sub content { shift->body(@_) }
+
+sub content_type { shift->headers->content_type(@_) }
+sub content_length { shift->headers->content_length(@_) }
+
+sub status {
+ my $self = shift;
+ if (@_) {
+ $self->{status} = shift;
+ }
+ else {
+ return $self->{status};
+ }
+}
+
+sub body {
+ my $self = shift;
+ if (@_) {
+ $self->{body} = shift;
+ }
+ else {
+ return $self->{body};
+ }
+}
+
+sub raw_body {
+ my $self = shift;
+ if (@_) {
+ $self->{raw_body} = shift;
+ }else{
+ return $self->{raw_body};
+ }
+}
+
+sub headers {
+ my $self = shift;
+ if (@_) {
+ my $headers = shift;
+ if ( ref $headers eq 'ARRAY' ) {
+ $headers = HTTP::Headers->new(@$headers);
+ }
+ elsif ( ref $headers eq 'HASH' ) {
+ $headers = HTTP::Headers->new(%$headers);
+ }
+ $self->{headers} = $headers;
+ }
+ else {
+ return $self->{headers} ||= HTTP::Headers->new();
+ }
+}
+
+sub request {
+ my $self = shift;
+ if (@_) {
+ $self->{request} = shift;
+ }else{
+ return $self->{request};
+ }
+}
+
+sub header {
+ my $self = shift;
+ $self->headers->header(@_);
+}
+
+sub finalize {
+ my $self = shift;
+ return [
+ $self->status,
+ +[
+ map {
+ my $k = $_;
+ map { ( $k => $_ ) } $self->headers->header($_);
+ } $self->headers->header_field_names
+ ],
+ $self->body,
+ ];
+}
+
+1;
diff --git a/lib/Net/HTTP/Spore/Role/Middleware.pm b/lib/Net/HTTP/Spore/Role/Middleware.pm
new file mode 100644
index 0000000..dd2c1c5
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Role/Middleware.pm
@@ -0,0 +1,44 @@
+package Net::HTTP::Spore::Role::Middleware;
+
+use Moose::Role;
+
+has middlewares => (
+ is => 'rw',
+ isa => 'ArrayRef',
+ traits => ['Array'],
+ lazy => 1,
+ default => sub { [] },
+ auto_deref => 1,
+ handles => { _add_middleware => 'push', _filter_middlewares => 'grep'},
+);
+
+sub _load_middleware {
+ my ( $self, $mw, @args ) = @_;
+
+ Class::MOP::load_class($mw);
+
+ my $code = $mw->wrap( @args );
+ $self->_add_middleware($code);
+}
+
+sub enable {
+ my ($self, $mw, @args) = @_;
+
+ if ($mw !~ /(?:^\+|Net\:\:HTTP\:\:Spore\:\:Middleware)/) {
+ $mw = "Net::HTTP::Spore::Middleware::".$mw;
+ }
+ $self->_load_middleware($mw, @args);
+ $self;
+}
+
+sub enable_if {
+ my ($self, $cond, $mw, @args) = @_;
+ $self;
+}
+
+sub reset_middlewares {
+ my $self = shift;
+ $self->middlewares([]);
+}
+
+1;
diff --git a/lib/Net/HTTP/Spore/Role/Request.pm b/lib/Net/HTTP/Spore/Role/Request.pm
new file mode 100644
index 0000000..840917a
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Role/Request.pm
@@ -0,0 +1,82 @@
+package Net::HTTP::Spore::Role::Request;
+
+# ABSTRACT: make HTTP request
+
+use Try::Tiny;
+use Moose::Role;
+use MooseX::Types::URI qw/Uri/;
+
+use Net::HTTP::Spore::Request;
+
+has api_base_url => (
+ is => 'rw',
+ isa => Uri,
+ coerce => 1,
+ required => 1,
+);
+
+sub http_request {
+ my ( $self, $env ) = @_;
+
+ my ($request, $response);
+ $request = Net::HTTP::Spore::Request->new($env);
+
+ my @middlewares;
+ foreach my $mw ( $self->middlewares ) {
+ my $res;
+ try {
+ $res = $mw->($request);
+ }
+ catch {
+ $res = $request->new_response( 599, [], { error => $_, } );
+ };
+
+ if ( ref $res && ref $res eq 'CODE' ) {
+ push @middlewares, $res;
+ }
+ elsif ( ref $res && ref $res eq 'Net::HTTP::Spore::Response' ) {
+ return $res if ($res->status == 599);
+ $response = $res;
+ last;
+ }
+ }
+
+ if (defined $response) {
+ map { $_->($response) } reverse @middlewares;
+ return $response;
+ }
+
+ my $result = $self->request($request->finalize);
+
+ $response = $request->new_response(
+ $result->code,
+ $result->headers,
+ $result->content,
+ );
+
+ map { $_->($response) } reverse @middlewares;
+
+ $response;
+}
+
+1;
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+=head2 METHODS
+
+=over 4
+
+=item B<http_request>
+
+=back
+
+=head2 ATTRIBUTES
+
+=over 4
+
+=item B<api_base_url>
+
+=back
diff --git a/lib/Net/HTTP/Spore/Role/UserAgent.pm b/lib/Net/HTTP/Spore/Role/UserAgent.pm
new file mode 100644
index 0000000..6bfaa5a
--- /dev/null
+++ b/lib/Net/HTTP/Spore/Role/UserAgent.pm
@@ -0,0 +1,22 @@
+package Net::HTTP::Spore::Role::UserAgent;
+
+# ABSTRACT: create UserAgent
+
+use Moose::Role;
+use LWP::UserAgent;
+
+has api_useragent => (
+ is => 'rw',
+ isa => 'LWP::UserAgent',
+ lazy => 1,
+ handles => [qw/request/],
+ default => sub {
+ my $self = shift;
+ my $ua = LWP::UserAgent->new();
+ $ua->agent( "Net::HTTP::Spore v" . $Net::HTTP::Spore::VERSION . " (Perl)" );
+ $ua->env_proxy;
+ return $ua;
+ }
+);
+
+1;
diff --git a/spec/spore.pod b/spec/spore.pod
new file mode 100644
index 0000000..3cd44d1
--- /dev/null
+++ b/spec/spore.pod
@@ -0,0 +1,105 @@
+=head1 NAME
+
+Spore - Specifications to a POrtable Rest Environment
+
+=head1 SYNOPSIS
+
+The ReST
+(L<http://en.wikipedia.org/wiki/Representational_State_Transfer|Representational
+State Transfer>) paradigm have improved the way we communicate between
+services and clients. It's easy to understand an API and to implement
+the communications.
+
+=head1 API DESCRIPTION
+
+An API should provide a description file. The description should be in JSON
+format.
+
+The description can have the following fields:
+
+=over 4
+
+=item B<name> (optional)
+
+A simple name to describe the specification (eg: CouchDB)
+
+=item B<author> (optional)
+
+A list of authors for this specification
+
+=item B<api_base_url> (optional)
+
+If the API have a fixed URL
+
+=item B<api_format> (optional)
+
+A list of supported format (eg: JSON, XML)
+
+=item B<version> (optinal)
+
+A version of the current description
+
+=item B<authentication> (optional)
+
+A boolean to inform if this API require authentication for all the methods
+
+=item B<methods> (required)
+
+A list of methods
+
+=back
+
+The desciption B<MUST> contains a list of at least one method.
+
+=over 4
+
+=item B<method> (required)
+
+An HTTP method (GET/POST/PUT/DELETE)
+
+=item B<path> (required)
+
+Path for the given method. The path can contains B<placeholder>. A placeholder
+B<MUST> begins with a <:>:
+
+ /:database
+
+=item B<params> (optional)
+
+A list of parameters. This list will be used to replace value in placeholder,
+and if not used in the path, will be added to the query.
+
+=item B<required> (optional)
+
+A list of required parameters. Parameters that are required B<MUST NOT> be
+repeted in the B<params> field.
+
+=item B<expected> (optional)
+
+A list of accepted HTTP status for this method. (eg: 200, 201).
+
+=item B<description> (optional)
+
+A simple description for the method. This should not be considered as
+documentation.
+
+ Fetch a document from a CouchDB database
+
+=item B<authentication> (optional)
+
+A boolean to define if this method requires authentication
+
+=item B<api_base_url> (optional)
+
+If this method require a different api_base_url
+
+=item B<documentation> (optional)
+
+A complete documentation for the given method
+
+=back
+
+=head3 SAMPLE
+
+=head3 CALLS
+
diff --git a/t/specs/couchdb.json b/t/specs/couchdb.json
new file mode 100644
index 0000000..f7c44b2
--- /dev/null
+++ b/t/specs/couchdb.json
@@ -0,0 +1,77 @@
+{
+ "version" : "0.1",
+ "methods" : {
+ "create_document_without_id" : {
+ "required" : [
+ "database"
+ ],
+ "path" : "/:database",
+ "method" : "POST"
+ },
+ "get_all_documents" : {
+ "params" : [
+ "descending",
+ "startkey",
+ "endkey",
+ "limit",
+ "include_docs"
+ ],
+ "required" : [
+ "database"
+ ],
+ "path" : "/:database/_all_docs",
+ "method" : "GET"
+ },
+ "create_document_with_id" : {
+ "required" : [
+ "database",
+ "doc_id"
+ ],
+ "path" : "/:database/:doc_id",
+ "method" : "POST"
+ },
+ "get_document" : {
+ "params" : [
+ "rev",
+ "revs"
+ ],
+ "required" : [
+ "database",
+ "doc_id"
+ ],
+ "path" : "/:database/:doc_id",
+ "method" : "GET"
+ },
+ "get_all_documents_by_seq" : {
+ "params" : [
+ "startkey",
+ "endkey",
+ "limit",
+ "include_docs"
+ ],
+ "required" : [
+ "database"
+ ],
+ "path" : "/:database/_all_docs_by_seq",
+ "method" : "GET"
+ },
+ "delete_document" : {
+ "params" : [
+ "rev"
+ ],
+ "required" : [
+ "database",
+ "doc_id"
+ ],
+ "path" : "/:database/:doc_id",
+ "method" : "DELETE"
+ }
+ },
+ "api_format" : [
+ "json"
+ ],
+ "name" : "CouchDB",
+ "author" : [
+ "franck cuny <franck@lumberjaph.net>"
+ ]
+}
diff --git a/t/spore-method/base.t b/t/spore-method/base.t
new file mode 100644
index 0000000..5010c38
--- /dev/null
+++ b/t/spore-method/base.t
@@ -0,0 +1,38 @@
+use strict;
+use warnings;
+use Test::More;
+use Test::Exception;
+use Net::HTTP::Spore::Meta::Method;
+
+dies_ok {
+ Net::HTTP::Spore::Meta::Method->wrap(
+ name => 'test_method',
+ package_name => 'test::api',
+ body => sub { 1 },
+ );
+}
+"missing some params";
+
+like $@, qr/Attribute \(method\) is required/;
+
+ok my $method = Net::HTTP::Spore::Meta::Method->wrap(
+ name => 'test_method',
+ package_name => 'test::api',
+ body => sub { 1 },
+ method => 'GET',
+ path => '/user/',
+ ),
+ 'method created';
+
+is $method->method, 'GET', 'method is GET';
+
+ok $method = Net::HTTP::Spore::Meta::Method->wrap(
+ name => 'test_method',
+ package_name => 'test::api',
+ method => 'GET',
+ path => '/user/',
+ params => [qw/name id street/],
+ required => [qw/name id/],
+);
+
+done_testing;
diff --git a/t/spore-middleware/auth-basic.t b/t/spore-middleware/auth-basic.t
new file mode 100644
index 0000000..92776ba
--- /dev/null
+++ b/t/spore-middleware/auth-basic.t
@@ -0,0 +1,32 @@
+use strict;
+use warnings;
+
+use Test::More;
+use MIME::Base64;
+
+use Net::HTTP::Spore;
+
+ok my $client =
+ Net::HTTP::Spore->new_from_spec( 't/specs/couchdb.json',
+ api_base_url => 'http://localhost:5984' );
+
+my $username = 'franck';
+my $password = 's3kr3t';
+
+$client->enable( 'Auth::Basic', username => $username, password => $password );
+$client->enable(
+ 'Test::Response',
+ body => 'result is ok',
+ headers => [ 'Content-Type' => 'text/html' ]
+);
+
+my $res = $client->get_all_documents( database => 'test_spore' );
+is $res->[0], 200;
+
+my $req = $res->request;
+
+is $req->header('Authorization'),
+ 'Basic ' . encode_base64( $username . ':' . $password, '' );
+
+done_testing;
+
diff --git a/t/spore-middleware/format-json.t b/t/spore-middleware/format-json.t
new file mode 100644
index 0000000..3e3b59b
--- /dev/null
+++ b/t/spore-middleware/format-json.t
@@ -0,0 +1,30 @@
+use strict;
+use warnings;
+
+use Test::More;
+use JSON;
+
+use Net::HTTP::Spore;
+
+ok my $client =
+ Net::HTTP::Spore->new_from_spec( 't/specs/couchdb.json',
+ api_base_url => 'http://localhost:5984' );
+
+my $content = { keys => [qw/1 2 3/] };
+
+$client->enable('Format::JSON');
+$client->enable(
+ 'Test::Response',
+ body => JSON::encode_json($content),
+ headers => [ 'Content-Type' => 'application/json' ]
+);
+
+my $res = $client->get_all_documents( database => 'test_spore' );
+is $res->[0], 200;
+is_deeply $res->[2], $content;
+is $res->header('Content-Type'), 'application/json';
+
+my $req = $res->request;
+is $req->header('Accept'), 'application/json';
+
+done_testing;
diff --git a/t/spore-middleware/format-xml.t b/t/spore-middleware/format-xml.t
new file mode 100644
index 0000000..0a01633
--- /dev/null
+++ b/t/spore-middleware/format-xml.t
@@ -0,0 +1,30 @@
+use strict;
+use warnings;
+
+use Test::More;
+use XML::Simple;
+
+use Net::HTTP::Spore;
+
+ok my $client =
+ Net::HTTP::Spore->new_from_spec( 't/specs/couchdb.json',
+ api_base_url => 'http://localhost:5984' );
+
+my $content = { keys => [qw/1 2 3/] };
+
+$client->enable('Format::XML');
+$client->enable(
+ 'Test::Response',
+ body => XMLout($content),
+ headers => [ 'Content-Type' => 'text/xml' ]
+);
+
+my $res = $client->get_all_documents( database => 'test_spore' );
+is $res->[0], 200;
+is_deeply $res->[2], $content;
+is $res->header('Content-Type'), 'text/xml';
+
+my $req = $res->request;
+is $req->header('Accept'), 'text/xml';
+
+done_testing;
diff --git a/t/spore-middleware/format-yaml.t b/t/spore-middleware/format-yaml.t
new file mode 100644
index 0000000..c104cc5
--- /dev/null
+++ b/t/spore-middleware/format-yaml.t
@@ -0,0 +1,30 @@
+use strict;
+use warnings;
+
+use Test::More;
+use YAML;
+
+use Net::HTTP::Spore;
+
+ok my $client =
+ Net::HTTP::Spore->new_from_spec( 't/specs/couchdb.json',
+ api_base_url => 'http://localhost:5984' );
+
+my $content = { keys => [qw/1 2 3/] };
+
+$client->enable('Format::YAML');
+$client->enable(
+ 'Test::Response',
+ body => Dump($content),
+ headers => [ 'Content-Type' => 'text/x-yaml' ]
+);
+
+my $res = $client->get_all_documents( database => 'test_spore' );
+is $res->[0], 200;
+is_deeply $res->[2], $content;
+is $res->header('Content-Type'), 'text/x-yaml';
+
+my $req = $res->request;
+is $req->header('Accept'), 'text/x-yaml';
+
+done_testing;
diff --git a/t/spore-middleware/runtime.t b/t/spore-middleware/runtime.t
new file mode 100644
index 0000000..d6c9b55
--- /dev/null
+++ b/t/spore-middleware/runtime.t
@@ -0,0 +1,19 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Net::HTTP::Spore;
+
+ok my $client =
+ Net::HTTP::Spore->new_from_spec( 't/specs/couchdb.json',
+ api_base_url => 'http://localhost:5984' );
+
+my $ua_str = 'Test::Spore middleware';
+
+$client->enable('Runtime');
+$client->enable('Test::Response');
+
+my $res = $client->get_all_documents(database => 'test_spore');
+ok $res->header('X-Spore-Runtime');
+
+done_testing;
diff --git a/t/spore-middleware/useragent.t b/t/spore-middleware/useragent.t
new file mode 100644
index 0000000..14dc9a6
--- /dev/null
+++ b/t/spore-middleware/useragent.t
@@ -0,0 +1,19 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Net::HTTP::Spore;
+
+ok my $client =
+ Net::HTTP::Spore->new_from_spec( 't/specs/couchdb.json',
+ api_base_url => 'http://localhost:5984' );
+
+my $ua_str = 'Test::Spore middleware';
+
+$client->enable('UserAgent', useragent => $ua_str);
+$client->enable('Test::Response');
+
+my $res = $client->get_all_documents(database => 'test_spore');
+is $res->request->header('User-Agent'), $ua_str;
+
+done_testing;
diff --git a/t/spore-request/base.t b/t/spore-request/base.t
new file mode 100644
index 0000000..7ae91e9
--- /dev/null
+++ b/t/spore-request/base.t
@@ -0,0 +1,71 @@
+use strict;
+use warnings;
+
+use Net::HTTP::Spore::Request;
+
+use Test::More;
+
+my @tests = (
+ {
+ host => 'localhost',
+ base => 'http://localhost/'
+ },
+ {
+ script_name => '/foo',
+ host => 'localhost',
+ base => 'http://localhost/foo'
+ },
+ {
+ script_name => '/foo bar',
+ host => 'localhost',
+ base => 'http://localhost/foo%20bar'
+ },
+ {
+ scheme => 'http',
+ host => 'localhost:91',
+ base => 'http://localhost:91/'
+ },
+ {
+ scheme => 'http',
+ host => 'example.com',
+ base => 'http://example.com/'
+ },
+ {
+ scheme => 'https',
+ host => 'example.com',
+ base => 'https://example.com/'
+ },
+ {
+ scheme => 'http',
+ server_name => 'example.com',
+ server_port => 80,
+ base => 'http://example.com/'
+ },
+ {
+ scheme => 'http',
+ server_name => 'example.com',
+ server_port => 8080,
+ base => 'http://example.com:8080/'
+ },
+ {
+ host => 'foobar.com',
+ server_name => 'example.com',
+ server_port => 8080,
+ base => 'http://foobar.com/'
+ },
+);
+
+plan tests => 1 * @tests;
+
+for my $block (@tests) {
+ my $env = {
+ 'spore.url_scheme' => $block->{scheme} || 'http',
+ HTTP_HOST => $block->{host} || undef,
+ SERVER_NAME => $block->{server_name} || undef,
+ SERVER_PORT => $block->{server_port} || undef,
+ SCRIPT_NAME => $block->{script_name} || '',
+ };
+
+ my $req = Net::HTTP::Spore::Request->new($env);
+ is $req->base, $block->{base};
+}
diff --git a/t/spore-request/exception.t b/t/spore-request/exception.t
new file mode 100644
index 0000000..162370a
--- /dev/null
+++ b/t/spore-request/exception.t
@@ -0,0 +1,17 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Net::HTTP::Spore;
+
+ok my $client =
+ Net::HTTP::Spore->new_from_spec( 't/specs/couchdb.json',
+ api_base_url => 'http://localhost:5984' );
+
+$client->enable( 'Test::Response', callback => sub { die } );
+
+my $res = $client->get_all_documents(database => 'test_spore');
+is $res->[0], 599;
+like $res->[2]->{error}, qr/Died/;
+
+done_testing;
diff --git a/t/spore-request/finalize.t b/t/spore-request/finalize.t
new file mode 100644
index 0000000..230c416
--- /dev/null
+++ b/t/spore-request/finalize.t
@@ -0,0 +1,27 @@
+use strict;
+use Test::More;
+
+use Net::HTTP::Spore::Request;
+
+my $env = {
+ REQUEST_METHOD => 'GET',
+ SERVER_NAME => 'localhost',
+ SERVER_PORT => '80',
+ SCRIPT_NAME => '',
+ PATH_INFO => '/:database',
+ REQUEST_URI => '',
+ QUERY_STRING => '',
+ SERVER_PROTOCOL => 'HTTP/1.0',
+ 'spore.params' => [qw/database test_spore key foo rev 123/],
+};
+
+ok my $request = Net::HTTP::Spore::Request->new($env);
+
+ok my $http_req = $request->finalize();
+isa_ok($http_req, 'HTTP::Request');
+
+is $env->{PATH_INFO}, '/test_spore';
+is $env->{QUERY_STRING}, 'key=foo&rev=123';
+is $http_req->uri->canonical, 'http://localhost/test_spore?key=foo&rev=123';
+
+done_testing;
diff --git a/t/spore-request/new.t b/t/spore-request/new.t
new file mode 100644
index 0000000..6cb9d56
--- /dev/null
+++ b/t/spore-request/new.t
@@ -0,0 +1,25 @@
+use strict;
+use Test::More;
+use Net::HTTP::Spore::Request;
+
+my $req = Net::HTTP::Spore::Request->new(
+ {
+ REQUEST_METHOD => 'GET',
+ SERVER_PROTOCOL => 'HTTP/1.1',
+ SERVER_PORT => 80,
+ SERVER_NAME => 'example.com',
+ SCRIPT_NAME => '/foo',
+ REMOTE_ADDR => '127.0.0.1',
+ 'spore.scheme' => 'http',
+ }
+);
+
+isa_ok( $req, 'Net::HTTP::Spore::Request' );
+
+is( $req->method, 'GET', 'method' );
+is( $req->protocol, 'HTTP/1.1', 'protocol' );
+is( $req->uri, 'http://example.com/foo', 'uri' );
+is( $req->port, 80, 'port' );
+is( $req->scheme, 'http', 'url_scheme' );
+
+done_testing();
diff --git a/t/spore-request/path_info.t b/t/spore-request/path_info.t
new file mode 100644
index 0000000..020a958
--- /dev/null
+++ b/t/spore-request/path_info.t
@@ -0,0 +1,25 @@
+use strict;
+use Test::More;
+
+use Net::HTTP::Spore::Request;
+
+my $env = {
+ REQUEST_METHOD => 'GET',
+ SERVER_NAME => 'localhost',
+ SERVER_PORT => '80',
+ SCRIPT_NAME => '',
+ PATH_INFO => '/:database/:key',
+ REQUEST_URI => '',
+ QUERY_STRING => '',
+ SERVER_PROTOCOL => 'HTTP/1.0',
+ 'spore.params' => [qw/database test_spore key foo/],
+};
+
+ok my $request = Net::HTTP::Spore::Request->new($env);
+
+is $request->path_info, '/test_spore/foo';
+
+$env->{'spore.params'} = [qw/database test_spore key foo another key/];
+is $request->path_info, '/test_spore/foo';
+
+done_testing;
diff --git a/t/spore-request/query_string.t b/t/spore-request/query_string.t
new file mode 100644
index 0000000..2ee7979
--- /dev/null
+++ b/t/spore-request/query_string.t
@@ -0,0 +1,25 @@
+use strict;
+use Test::More;
+
+use Net::HTTP::Spore::Request;
+
+my $env = {
+ REQUEST_METHOD => 'GET',
+ SERVER_NAME => 'localhost',
+ SERVER_PORT => '80',
+ SCRIPT_NAME => '',
+ PATH_INFO => '/:database',
+ REQUEST_URI => '',
+ QUERY_STRING => '',
+ SERVER_PROTOCOL => 'HTTP/1.0',
+ 'spore.params' => [qw/database test_spore key foo rev 123/],
+};
+
+ok my $request = Net::HTTP::Spore::Request->new($env);
+
+is $request->query_string, 'key=foo&rev=123';
+
+$env->{PATH_INFO} = '/:database/:key';
+is $request->query_string, 'rev=123';
+
+done_testing;
diff --git a/t/spore-request/uri.t b/t/spore-request/uri.t
new file mode 100644
index 0000000..d3f8b82
--- /dev/null
+++ b/t/spore-request/uri.t
@@ -0,0 +1,109 @@
+use strict;
+use warnings;
+use Test::More;
+
+use Net::HTTP::Spore::Request;
+
+my @tests = (
+ {
+ add_env => {
+ HTTP_HOST => 'example.com',
+ SCRIPT_NAME => "",
+ },
+ uri => 'http://example.com/',
+ parameters => {}
+ },
+ {
+ add_env => {
+ HTTP_HOST => 'example.com',
+ SCRIPT_NAME => "",
+ PATH_INFO => "/foo bar",
+ },
+ uri => 'http://example.com/foo%20bar',
+ parameters => {}
+ },
+ {
+ add_env => {
+ HTTP_HOST => 'example.com',
+ SCRIPT_NAME => '/test.c',
+ },
+ uri => 'http://example.com/test.c',
+ parameters => {}
+ },
+ {
+ add_env => {
+ HTTP_HOST => 'example.com',
+ SCRIPT_NAME => '/test.c',
+ PATH_INFO => '/info',
+ },
+ uri => 'http://example.com/test.c/info',
+ parameters => {}
+ },
+ {
+ add_env => {
+ HTTP_HOST => 'example.com',
+ SCRIPT_NAME => '/test',
+ 'spore.params' => [qw/dynamic daikuma/],
+ },
+ uri => 'http://example.com/test?dynamic=daikuma',
+ parameters => { dynamic => 'daikuma' }
+ },
+ {
+ add_env => {
+ HTTP_HOST => 'example.com',
+ SCRIPT_NAME => '/exec/'
+ },
+ uri => 'http://example.com/exec/',
+ parameters => {}
+ },
+ {
+ add_env => { SERVER_NAME => 'example.com' },
+ uri => 'http://example.com/',
+ parameters => {}
+ },
+ {
+ add_env => {},
+ uri => 'http:///',
+ parameters => {}
+ },
+ {
+ add_env => {
+ HTTP_HOST => 'example.com',
+ SCRIPT_NAME => "",
+ 'spore.params' => [qw/aco tie/],
+ },
+ uri => 'http://example.com/?aco=tie',
+ parameters => { aco => 'tie' }
+ },
+ {
+ add_env => {
+ HTTP_HOST => 'example.com',
+ SCRIPT_NAME => "",
+ 'spore.params' => [qw/0/],
+ },
+ uri => 'http://example.com/?0',
+ parameters => {}
+ },
+ {
+ add_env => {
+ HTTP_HOST => 'example.com',
+ SCRIPT_NAME => "/foo bar",
+ PATH_INFO => "/baz quux",
+ },
+ uri => 'http://example.com/foo%20bar/baz%20quux',
+ parameters => {}
+ }
+);
+
+plan tests => 1 * @tests;
+
+for my $block (@tests) {
+ my $env = { SERVER_PORT => 80 };
+ while ( my ( $key, $val ) = each %{ $block->{add_env} || {} } ) {
+ $env->{$key} = $val;
+ }
+ my $req = Net::HTTP::Spore::Request->new($env);
+
+ is $req->uri, $block->{uri};
+# is_deeply $req->query_parameters, $block->{parameters};
+}
diff --git a/t/spore-response/body.t b/t/spore-response/body.t
new file mode 100644
index 0000000..2a35d6b
--- /dev/null
+++ b/t/spore-response/body.t
@@ -0,0 +1,21 @@
+use strict;
+use warnings;
+use Test::More;
+use Net::HTTP::Spore::Response;
+use URI;
+
+sub r($) {
+ my $res = Net::HTTP::Spore::Response->new(200);
+ $res->body(@_);
+ return $res->finalize->[2];
+}
+
+is_deeply r "Hello World", "Hello World";
+is_deeply r [ "Hello", "World" ], [ "Hello", "World" ];
+
+{
+ my $uri = URI->new("foo"); # stringified object
+ is_deeply r $uri, $uri;
+}
+
+done_testing;
diff --git a/t/spore-response/headers.t b/t/spore-response/headers.t
new file mode 100644
index 0000000..b9cf319
--- /dev/null
+++ b/t/spore-response/headers.t
@@ -0,0 +1,20 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Net::HTTP::Spore::Response;
+
+my $status = 200;
+my $body = '{"foo":1}';
+my $ct = 'application/json';
+my $cl = length($body);
+
+my $response =
+ Net::HTTP::Spore::Response->new( $status,
+ [ 'Content-Type', $ct, 'Content-Length', length($body) ], $body );
+
+is $response->content_type, $ct;
+is $response->content_length, $cl;
+is $response->status, 200;
+
+done_testing;
diff --git a/t/spore-response/new.t b/t/spore-response/new.t
new file mode 100644
index 0000000..fb271ab
--- /dev/null
+++ b/t/spore-response/new.t
@@ -0,0 +1,34 @@
+use strict;
+use warnings;
+use Test::More;
+use Net::HTTP::Spore::Response;
+
+{
+ my $res = Net::HTTP::Spore::Response->new(302);
+ is $res->status, 302;
+ is $res->code, 302;
+}
+
+{
+ my $res = Net::HTTP::Spore::Response->new(200, [ 'Content-Type' => 'text/plain' ]);
+ is $res->content_type, 'text/plain';
+}
+
+{
+ my $res = Net::HTTP::Spore::Response->new(200, { 'Content-Type' => 'text/plain' });
+ is $res->content_type, 'text/plain';
+}
+
+{
+ my $res = Net::HTTP::Spore::Response->new(200);
+ $res->content_type('image/png');
+ is $res->content_type, 'image/png';
+}
+
+{
+ my $res = Net::HTTP::Spore::Response->new(200);
+ $res->header('X-Foo' => "bar");
+ is $res->header('X-Foo'), "bar";
+}
+
+done_testing;
diff --git a/t/spore-response/response.t b/t/spore-response/response.t
new file mode 100644
index 0000000..56be6d2
--- /dev/null
+++ b/t/spore-response/response.t
@@ -0,0 +1,23 @@
+use strict;
+use warnings;
+use Test::More;
+use Net::HTTP::Spore::Response;
+
+sub res {
+ my $res = Net::HTTP::Spore::Response->new;
+ my %v = @_;
+ while ( my ( $k, $v ) = each %v ) {
+ $res->$k($v);
+ }
+ $res->finalize;
+}
+
+is_deeply(
+ res(
+ status => 200,
+ body => 'hello',
+ ),
+ [ 200, +[], 'hello' ]
+);
+
+done_testing;