root/lang/perl/POE-Component-Server-JSONRPC/trunk/lib/POE/Component/Server/JSONRPC.pm @ 14290

Revision 14290, 5.9 kB (checked in by nothingmuch, 5 years ago)

improve error handling

Line 
1package POE::Component::Server::JSONRPC;
2use MooseX::POE;
3
4use JSON::RPC::Common::Message;
5use JSON::RPC::Common::Procedure::Call;
6use JSON::RPC::Common::Procedure::Return;
7
8our $VERSION = '0.02';
9
10use POE qw(
11    Component::Server::TCP
12    Filter::Stackable
13    Filter::JSON::Incr
14    Filter::JSONRPC::Stream
15);
16
17use namespace::clean -except => [qw(meta)];
18
19with qw(MooseX::POE::Aliased);
20
21=head1 NAME
22
23POE::Component::Server::JSONRPC - POE tcp based JSON-RPC server
24
25=head1 SYNOPSIS
26
27    use POE qw(Component::Server::JSONRPC);
28
29    POE::Session->create(
30        inline_states => {
31            _start => sub {
32                POE::Component::Server::JSONRPC->new(
33                    Port    => 3000,
34                    Handler => {
35                        'echo' => 'echo',
36                        'sum'  => 'sum',
37                    },
38                );
39            },
40            echo => \&echo,
41            sum => \&sum,
42        },
43    );
44
45    sub echo {
46        my ($kernel, $jsonrpc, @params) = @_[KERNEL, ARG0..$#_ ];
47   
48        $kernel->post( $jsonrpc => 'result' => @params );
49    }
50   
51    sub sum {
52        my ($kernel, $jsonrpc, @params) = @_[KERNEL, ARG0..$#_ ];
53   
54        $kernel->post( $jsonrpc => 'result' => $params[0] + $params[1] );
55    }
56
57=head1 DESCRIPTION
58
59This module is a POE component for tcp based JSON-RPC Server.
60
61The specification is defined on http://json-rpc.org/ and this module use JSON-RPC 1.0 spec (1.1 does not cover tcp streams)
62
63=head1 METHODS
64
65=head2 new
66
67Create JSONRPC component session and return the session id.
68
69Parameters:
70
71=over
72
73=item Handler
74
75Hash variable contains handler name as key, handler poe state name as value.
76
77Handler name (key) is used as JSON-RPC method name.
78
79So if you send {"method":"echo"}, this module call the poe state named "echo".
80
81=back
82
83=cut
84
85has handler => (
86    isa => "CodeRef|Str|HashRef",
87    is  => "rw",
88);
89
90has _parent => (
91    default => sub { $poe_kernel->get_active_session->ID },
92    is  => "rw",
93);
94
95has _tcp => (
96    is  => "rw",
97);
98
99has tcp => (
100    isa => "HashRef",
101    is  => "rw",
102    required => 1,
103);
104
105has default_version => (
106    isa => "Str",
107    is  => "rw",
108    default => sub { "1.0" }
109);
110
111has return_class => (
112    isa => "ClassName",
113    is  => "rw",
114    lazy_build => 1,
115    handles => { "create_return" => "new_from_data" },
116);
117
118sub _build_return_class {
119    my $self = shift;
120
121    my $class = JSON::RPC::Common::Procedure::Return->_version_class( $self->default_version );
122
123    Class::MOP::load_class($class);
124
125    return $class;
126}
127
128=head1 HANDLER PARAMETERS
129
130=over
131
132=item ARG0
133
134A callback you can invoke to return results.
135
136=item ARG1
137
138The L<JSON::RPC::Common> object.
139
140=back
141
142=head1 HANDLER RESPONSE
143
144    $_[ARG0]->( "result value" );
145
146If you do above, response is:
147
148   {"result":"result value", "error":"", id:...}
149
150=head1 EVENTS
151
152Two events same as PoCo::Server::TCP are supported.
153
154=head2 shutdown
155
156Shuts down TCP server.
157
158=cut
159
160event shutdown => sub {
161    my ($self, $kernel) = @_[OBJECT, KERNEL];
162    $kernel->post( $self->_tcp => 'shutdown' );
163};
164
165=head2 set_concurrency
166
167Set the number of simultaneous connections
168
169For more infomation about these events, see L<POE::Component::Server::TCP>.
170
171=cut
172
173event set_concurrency => sub {
174    my ($self, $kernel, @args) = @_[OBJECT, KERNEL, ARG0..$#_];
175    $kernel->post( $self->_tcp => set_concurrency => @args );
176};
177
178=head1 POE METHODS
179
180Inner method for POE states.
181
182=head2 poe__start
183
184=cut
185
186sub START {
187    my ($self, $kernel, $session) = @_[OBJECT, KERNEL, SESSION];
188
189    my $f = POE::Filter::Stackable->new(
190        Filters => [
191            POE::Filter::JSON::Incr->new(
192                errors => 1,
193                json_opts => [qw(utf8)]
194            ),
195            POE::Filter::JSONRPC::Stream->new(),
196        ],
197    );
198
199    $self->_tcp(
200        POE::Component::Server::TCP->new(
201            %{ $self->tcp },
202            ClientInput  => sub { $_[KERNEL]->call( $session => tcp_input_handler => $_[HEAP]{client}, @_[ARG0..$#_]) },
203            ClientFilter => $f,
204        ),
205    );
206}
207
208=head2 poe_tcp_input_handler
209
210=cut
211
212event tcp_input_handler => sub {
213    my ($self, $kernel, $client, $msg) = @_[OBJECT, KERNEL, ARG0, ARG1 ];
214
215    if ( blessed($msg) ) {
216        if ( $msg->isa("JSON::RPC::Common::Procedure::Call") ) {
217            my $handler = $self->handler;
218
219            if ( ref($handler) eq 'HASH' ) {
220                unless ($handler = $handler->{ $msg->method }) {
221                    $kernel->yield('error', $client, sprintf qq{no such method "%s"}, $msg->method, $msg);
222                    return;
223                }
224            }
225
226            my $cb = sub {
227                if ( do { local $@; eval { $_[0]->does("JSON::RPC::Common::Message") } } ) {
228                    $client->put($_[0]);
229                } else {
230                    $client->put( $msg->return_result(@_) );
231                }
232            };
233
234            if ( ref $handler ) {
235                $handler->( $cb, $msg, $client );
236            } else {
237                $kernel->post( $self->_parent, $handler, $cb, $msg, $client );
238            }
239        } else {
240            # FIXME result, track req IDs, map back
241        }
242    } else {
243        $msg = (split /\n/, $msg)[0];
244        $msg =~ s/ at .+? line \d+//;
245        $kernel->yield(error => $client, $msg);
246    }
247};
248
249=head2 poe_error
250
251=cut
252
253event error => sub {
254    my ($self, $kernel, $client, $error, $call) = @_[OBJECT, KERNEL, ARG0 .. ARG2];
255
256    $client->put( $call ? $call->return_error($error) : $self->new_error($error) );
257};
258
259sub new_error {
260    my ( $self, @args ) = @_;
261
262    $self->create_return(
263        id    => undef,
264        error => ( @args % 2 == 0 ? {@args} : $args[0] || "Invalid request" ),
265    );
266};
267
268=head1 AUTHOR
269
270Daisuke Murase <typester@cpan.org>
271
272=head1 COPYRIGHT
273
274This program is free software; you can redistribute
275it and/or modify it under the same terms as Perl itself.
276
277The full text of the license can be found in the
278LICENSE file included with this module.
279
280=cut
281
2821;
Note: See TracBrowser for help on using the browser.