package POE::Component::Server::JSONRPC; use strict; use warnings; use base qw/Class::Accessor::Fast/; our $VERSION = '0.02'; use POE qw/ Component::Server::TCP Filter::Line /; use JSON::Any; =head1 NAME POE::Component::Server::JSONRPC - POE tcp based JSON-RPC server =head1 SYNOPSIS POE::Component::Server::JSONRPC->new( Port => 3000, Handler => { 'echo' => 'echo', 'sum' => 'sum', }, ); sub echo { my ($kernel, $jsonrpc, @params) = @_[KERNEL, ARG0..$#_ ]; $kernel->post( $jsonrpc => 'result' => @params ); } sub sum { my ($kernel, $jsonrpc, @params) = @_[KERNEL, ARG0..$#_ ]; $kernel->post( $jsonrpc => 'result' => $params[0] + $params[1] ); } =head1 DESCRIPTION This module is a POE component for tcp based JSON-RPC Server. The specification is defined on http://json-rpc.org/ and this module use JSON-RPC 1.0 spec (1.1 does not cover tcp streams) =head1 METHODS =head2 new Create JSONRPC component session and return the session id. Parameters: =over =item Port Port number for listen. =item Handler Hash variable contains handler name as key, handler poe state name as value. Handler name (key) is used as JSON-RPC method name. So if you send {"method":"echo"}, this module call the poe state named "echo". =back =cut sub new { my $self = shift->SUPER::new( @_ > 1 ? {@_} : $_[0] ); $self->{parent} = $poe_kernel->get_active_session->ID; $self->{json} ||= JSON::Any->new; my $session = POE::Session->create( object_states => [ $self => { map { ( $_ => "poe_$_", ) } qw/_start tcp_input_handler result error/ }, ], ); $session->ID; } =head1 HANDLER PARAMETERS =over =item ARG0 A session id of PoCo::Server::JSONRPC itself. =item ARG1 .. ARGN JSONRPC argguments =back ex) If you send following request {"method":"echo", "params":["foo", "bar"]} then, "echo" handler is called and parameters is that ARG0 is component session id, ARG1 "foo", ARG2 "bar". =head1 HANDLER RESPONSE You must call either "result" or "error" state in your handlers to response result or error. ex: $kernel->post( $component_session_id => "result" => "result value" ) $component_session_id is ARG0 in handler. If you do above, response is: {"result":"result value", "error":""} =head1 POE METHODS Inner method for POE states. =head2 poe__start =cut sub poe__start { my ($self, $kernel, $session) = @_[OBJECT, KERNEL, SESSION]; my $bind = sub { my $method = $_[0]; return sub { my ($kernel, $tcp_session, @args) = @_[KERNEL, SESSION, ARG0..$#_ ]; $kernel->post( $session->ID, $method, $tcp_session->ID, @args ); }; }; $self->{tcp} = POE::Component::Server::TCP->new( Port => $self->{Port}, $self->{Address} ? ( Address => $self->{Address} ) : (), $self->{Hostname} ? ( Hostname => $self->{Hostname} ) : (), $self->{Domain} ? ( Domain => $self->{Domain} ) : (), $self->{Concurrency} ? ( Concurrency => $self->{Concurrency} ) : (), ClientInput => $bind->('tcp_input_handler'), # ClientConnected => $bind->('tcp_connect_handler'), # ClientDisconnected => $bind->('tcp_disconnect_handler'), # ClientError => $bind->('tcp_client_error_handler'), # ClientFlushed => $bind->('tcp_client_flush_handler'), ClientInputFilter => $self->{ClientInputFilter} || POE::Filter::Line->new, ClientOutputFilter => $self->{ClientOutputFilter} || POE::Filter::Line->new, InlineStates => { send => sub { my ($heap, $data) = @_[HEAP, ARG0]; $heap->{client}->put($data) if $heap->{client}; }, }, ); } =head2 poe_tcp_input_handler =cut sub poe_tcp_input_handler { my ($self, $kernel, $session, $heap, $client, @args) = @_[OBJECT, KERNEL, SESSION, HEAP, ARG0..$#_ ]; $heap->{client} = $client; my $json; eval { $json = $self->{json}->Load( $args[0] ); }; if ($@) { $kernel->yield('error', q{invalid json request}); return; } unless ($json and $json->{method}) { $kernel->yield('error', q{parameter "method" is required}); return; } unless ($self->{Handler}{ $json->{method} }) { $kernel->yield('error', qq{no such method "$json->{method}"}); return; } $heap->{id} = $json->{id}; my $handler = $self->{Handler}{ $json->{method} }; my @params = @{ $json->{params} || [] }; $kernel->post( $self->{parent}, $handler, $session->ID, @params ); } =head2 poe_result =cut sub poe_result { my ($self, $kernel, $heap, @results) = @_[OBJECT, KERNEL, HEAP, ARG0..$#_ ]; $kernel->post( $heap->{client} => send => $self->{json}->Dump( { id => $heap->{id} || undef, error => undef, result => (@results > 1 ? \@results : $results[0]), } ), ); } =head2 poe_error =cut sub poe_error { my ($self, $kernel, $heap, $error) = @_[OBJECT, KERNEL, HEAP, ARG0]; $kernel->post( $heap->{client} => send => $self->{json}->Dump( { id => $heap->{id} || undef, error => $error, result => undef, } ), ); } =head1 AUTHOR Daisuke Murase <typester@cpan.org> =head1 COPYRIGHT This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. The full text of the license can be found in the LICENSE file included with this module. =cut 1;