package POE::Component::Client::REST; use POE qw(Component::Client::HTTP); use Moose; use HTTP::Request; our $VERSION = '0.05'; has Alias => ( is => 'ro', default => 'REST-Session', ); has http => ( is => 'ro', lazy => 1, default => sub { my $str = $_[0]->Alias; "$str-HTTP" }, ); has request_cooker => ( is => 'ro', isa => 'Maybe[CodeRef]', ); has response_cooker => ( is => 'ro', isa => 'Maybe[CodeRef]', ); sub BUILD { my ($self, $args) = @_; my %object_states = map { $_ => "_$_" } qw(call shutdown respond); POE::Session->create( inline_states => {_start => sub { $poe_kernel->alias_set($self->Alias) }}, object_states => [$self => \%object_states] ); $args->{Alias} = $self->http; POE::Component::Client::HTTP->spawn(%$args); } sub spawn { my $self = shift; $self->new(@_) } sub call { my ($self, $method, $url, @rest) = @_; my $opts = (@rest > 0 ? $rest[0] eq 'HASH' ? $rest[0] : {@rest} : {}); $poe_kernel->post($self->Alias, call => $method, $url, $opts); } sub _call { my ($self, $heap, $method, $url, $opts) = @_[OBJECT, HEAP, ARG0..ARG3]; if (my $query = $opts->{query}) { my @pairs = map { my ($k, $v) = ($_, $query->{$_}); "$k=$v" } keys (%$query); $url = join('?', $url, join('&', @pairs)); } my $request = HTTP::Request->new( $method, $url, $opts->{headers}, $opts->{content}); my $cooker = exists $opts->{request_cooker} ? $opts->{request_cooker} : $self->request_cooker; $request = $cooker->($request) if $cooker; if(my $cont = $opts->{callback}) { $heap->{request_info}->{$request}->{continuation} = $cont; } $heap->{request_info}->{$request}->{cooker} = exists $opts->{response_cooker} ? $opts->{response_cooker} : $self->response_cooker; $poe_kernel->post($self->http, request => respond => $request); } sub _respond { my ($heap, $request_packet, $response_packet) = @_[HEAP, ARG0..ARG1]; my $req = $request_packet->[0]; my @res = ($response_packet->[0]); my $info = delete $heap->{request_info}->{$req}; my $c = $info->{continuation} || return; my $cooker = $info->{cooker}; @res = $cooker->(@res) if $cooker; if (ref $c eq 'CODE') { $c->(@res); } else { $poe_kernel->post($c->[0], $c->[1], @res); } } sub shutdown { $poe_kernel->post($_[0]->Alias, 'shutdown'); } sub _shutdown { my $self = $_[OBJECT]; $poe_kernel->post($self->http, 'shutdown'); $poe_kernel->alias_remove($self->Alias); } 1; __END__ =head1 NAME POE::Component::Client::REST - Low-level interface for REST calls =head1 VERSION Version 0.05 =head1 SYNOPSIS This class abstracts away some of the nastier details of talking to a REST service. use POE qw(Component::Client::REST::JSON); # simple CouchDB example POE::Session->create(inline_states => { _start => sub { $poe_kernel->alias_set('foo'); my $rest = $_[HEAP]->{rest} = POE::Component::Client::REST::JSON->new; $rest->call(GET => 'http://localhost:5984/_all_dbs', callback => [$_[SESSION], 'response']); }, response => sub { my ($data, $response) = @_[ARG0, ARG1]; die $response->status_line unless $response->code == 200; print 'Databases: ' . join(', ', @$data) . "\n"; $poe_kernel->alias_remove('foo'); $_[HEAP]->{rest}->shutdown(); }, }); $poe_kernel->run(); =head1 ATTRIBUTES =over 4 =item Alias The string to use as the alias for the internal session. =item http The alias of the spawned HTTP object's session. Defaults to something reasonable. =item request_cooker A function which takes a request and cooks it in some fashion, returning a new one (or the same one, modified). Undefined by default, but see L. =item response_cooker Similar to the above, but returns a list of things to be passed to the callbacks when a response is received for a particular request. =back =head1 METHODS =head2 call I Makes an HTTP request to I via I. The following keyword arguments are accepted (either as a hashref or just as extra argument pairs). =over 4 =item request_cooker =item response_cooker Overrides the object defaults for these values. Explicitly pass undef to specify that no cooking should be done. =item query A hashref of query parameters to be appended as a query string. =item content Data (which may or may not get cooked) to be passed as the request body. =item headers Specify arbitrary headers as an arrayref of key-value pairs. =item callback Either a coderef or an arrayref of two elements (session and state name) to call (or post to) with the (possibly cooked) response to the request. =back You can also post to this session's 'call' state for the same result, although in this form the keywords must be a hashref. =head2 spawn I =head2 new I Passes all options to L except for Alias, which is used for our own session. The HTTP session has an alias of "$Alias-HTTP". The default session alias for the internal session is REST-JSON-Session. =head2 shutdown Calls shutdown on the HTTP client and stops this session. You can also post to the 'shutdown' state for the same result. =head1 AUTHOR Paul Driver, C<< >> =head1 BUGS This was written for use with L, so it's sort of tailored to that API and probably unsuitable without modification for other purposes. Patches welcome! =head1 COPYRIGHT & LICENSE Copyright 2008 Paul Driver This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.