use MooseX::Role::Parameterized;
use MooseX::Role::REST::Consumer::Response;
use File::Spec;
use REST::Consumer;
use Try::Tiny;
use URI::Escape;
use Module::Load;
parameter query_params_mapping => (
isa => 'HashRef',
default => sub {{}},
);
parameter useragent_class => (
isa => 'Str',
);
role {
my $p = shift;
my $service_host = $p->service_host;
my $service_port = $p->service_port;
my $resource_path = $p->resource_path;
my $content_type = $p->content_type;
my $retry = $p->retry;
my $timeout = $p->timeout;
my $query_params_mapping = $p->query_params_mapping;
my $useragent_class = $p->useragent_class;
#Note: since this is a parametrized role then only one class will be closing
#over this variable and this instance only depends on role parameters
my $consumer_instance;
#Note: $class varible in methods of this role can be an instance and a class name
#For GET requests this is usually a class name, for POST requests
#this would usually be an instance.
#Be carefull!
method 'consumer' => sub {
my ($class) = @_;
return $consumer_instance if($consumer_instance);
# TODO: It's confusing as to how we handle errors.
# ie: message should be called "error_message" and
# we should inspect the response content for possible error messages
return MooseX::Role::REST::Consumer::Response->new(
data => $data,
is_success => !$error,
error_message => "$error",
request => $consumer->last_request,
content_type => $content_type,
response => $consumer->last_response,
);
};
# Note: REST::Consumer doesn't support OPTIONS/PATCH
for my $method ( @HTTP_METHODS ) {
method $method => sub {
my $class = shift;
$class->call($method, @_);
}
}
method 'request_headers' => sub {
my ($class, %params) = @_;
my %request_headers = ($params{headers} ? %{delete $params{headers}} : ());
# Strange mutation going on here:
# 1. First we set the content_type in the headers if we have one set in parameter definition
# 2. However, we won't override anything that is passed explicitly into a method call Class->post
# 3. Next we delete anything that needs to be removed from the header
# 4. Finally we explicltly pull out content-type from the request_headers
# to make REST::Consumer happy
$request_headers{'Content-Type'} = $params{content_type} unless $request_headers{'Content-Type'};
delete @request_headers{@{$params{header_exclude}->{$params{method}}}} if $params{header_exclude}->{$params{method}};
return %request_headers;
};
method 'request_path' => sub {
my ($class, %params) = @_;
my $resource_path = $params{resource_path};
my $route_params = $params{route_params};
my $params_path = $params{path};
my @path = (defined $resource_path ? $resource_path : ());
if($params_path) {
if(ref($params_path) eq 'ARRAY') {
push(@path, @$params_path);
} else {
push(@path, $params_path);
}
}
my $path = File::Spec->catfile(@path) . ( $resource_path && $resource_path =~ m{[^/]+/$} ? '/' : '');
#We support two ways of substituting params here:
# /:param/ - name has to have '/' or end of string after it
# /has_:{param}_value - surround param name with '{}'
#Note: we go through complete incremental parsing/fetching url params to avoid
#params substituted values being mached against other params names
#We also verify that all parameters have been substituted
my $result = '';
while($path =~ /\G(.*?)(:(?:\{([^}]+?)\}|(\w+)(?=\W|$)))/gc) {
$result .= $1;
if($2) {
my $name = $3 || $4; # only one of them could match
if(exists $route_params->{$name}) {
$result .= URI::Escape::uri_escape_utf8($route_params->{$name} // '');
}
else {
die "Found parameter $name in path but it wasn't set in parameters hash";
}
}
}
if($path =~ /\G(.+)$/g) {
$result .= $1;
}
return $result;
};
method 'query_params' => sub {
my ($class, %params) = @_;
my %query_params;
if($params{params}) {
# TODO: I don't think having this strict mapping in place
# makes a lot of sense. Ideally we should be able to pass in
# any query params that we want
if ( $params{query_params_mapping} ) {
while(my ($name, $url_name) = each(%{$params{query_params_mapping}})) {
# Check for method canness here probably
$query_params{$url_name} = delete($params{params}->{$name});
}
} else {
%query_params = %{$params{params}};
}
}
return %query_params;
};
method 'parameters' => sub { $p };
};
__END__
=pod
=head1 NAME
MooseX::Role::REST::Consumer
=head1 VERSION
version 0.001
=head1 SYNOPSIS
package Foo;
use Moose;
with 'MooseX::Role::REST::Consumer' => {
service_host => 'somewhere.over.the.rainbow',
resource_path => '/path/to/my/resource/:id',
};
my $object = Foo->get(route_params => {id => 1});
if ($object->is_success) {
print $object->data->{something_that_came_back};
}
=head2 DESCRIPTION
At Shutterstock we love REST and we take it so seriously that we think
our code should be RESTfully lazy. Now one can have a Moose model
without needing to deal with all the marshalling details.
=head3 Schema Definitions/Configuration
When setting up a class the following are the supported
parameters that L<MooseX::Role::REST::Consumer> will support.
For example a typical configuration would looke like the following:
Will perform a POST request with REST::Consumer::post.
The data will the Content-Type of application/json by default.
=item Other supported HTTP methods
DELETE and PUT: delete(%params) and put(%params)
=back
=head3 Supported Method Parameters
=over
=item route_params => {...}
These will be substituted into the package route definition.
=item params => {...}
Passed into L<REST::Consumer> as a set of key/value
query parameters.
=item headers => {...}
Any extra HTTP request headers to send.
=item content => ''
Passed into L<REST::Consumer>. This is the body content of a request.
=item timeout => ''
Timeout override per request.
Note that the 'timeout' is subject to interpretation
by your underlying UserAgent class. For example,
LWP::UserAgent treats the timeout as being
C<per-request>. This means that if you specify a timeout
of 5 seconds and issue a request using LWP::UserAgent,
each request that the UserAgent makes to fulfill your
request will have its own timeout of 5 seconds.
This becomes important if the API that you are talking to
starts giving you 3xx redirects: while you might expect a
timeout to occur within 5 seconds, the API might instruct
your UserAgent to make a few subsequent requests, and each
one will have your initial timeout applied to it.
Different UserAgent classes implement timeouts
differently. L<LWP::UserAgent::Paranoid>, for example,
has a global timeout value, where all requests must be
fulfilled within C<timeout> clock seconds.
=back
=head3 Response Object
=over
The response object is created and passed back whenever
any of the supported HTTP methods are called.
See L<MooseX::Role::REST::Consumer::Response>.