File Coverage

blib/lib/JSON/RPC/Dispatcher.pm
Criterion Covered Total %
statement 2 4 50.0
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 4 6 66.6


line stmt bran cond sub pod time code
1             package JSON::RPC::Dispatcher;
2             BEGIN {
3 1     1   802 $JSON::RPC::Dispatcher::VERSION = '0.0506';
4             }
5              
6             =head1 NAME
7              
8             JSON::RPC::Dispatcher - A JSON-RPC 2.0 server.
9              
10             =head1 VERSION
11              
12             version 0.0506
13              
14             =head1 SYNOPSIS
15              
16             In F<app.psgi>:
17              
18             use JSON::RPC::Dispatcher;
19              
20             my $rpc = JSON::RPC::Dispatcher->new;
21              
22             sub add_em {
23             my @params = @_;
24             my $sum = 0;
25             $sum += $_ for @params;
26             return $sum;
27             }
28             $rpc->register( 'sum', \&add_em );
29              
30             $rpc->to_app;
31              
32             Then run it:
33              
34             plackup app.psgi
35              
36             Now you can then call this service via a GET like:
37              
38             http://example.com/?method=sum;params=[2,3,5];id=1
39              
40             Or by posting JSON to it like this:
41              
42             {"jsonrpc":"2.0","method":"sum","params":[2,3,5],"id":"1"}
43              
44             And you'd get back:
45              
46             {"jsonrpc":"2.0","result":10,"id":"1"}
47            
48             =head1 DESCRIPTION
49              
50             Using this app you can make any PSGI/L<Plack> aware server a JSON-RPC 2.0 server. This will allow you to expose your custom functionality as a web service in a relatiely tiny amount of code, as you can see above.
51              
52             This module follows the draft specficiation for JSON-RPC 2.0. More information can be found at L<http://groups.google.com/group/json-rpc/web/json-rpc-2-0>.
53              
54             =head2 Registration Options
55              
56             The C<register> method takes a third argument which is a hash reference of named options that effects how the code should be handled.
57              
58             =head3 with_plack_request
59              
60             The first argument passed into the function will be a reference to the Plack::Request object, which is great for getting environment variables, and HTTP headers if you need those things in processing your RPC.
61              
62             $rpc->register( 'some_func', \&some_func, { with_plack_request => 1 });
63              
64             sub some_func {
65             my ($plack_request, $other_arg) = @_;
66             ...
67             }
68              
69             B<TIP:> Before using this option consider whether you might be better served by a L<Plack::Middleware> component. For example, if you want to do HTTP Basic Auth on your requests, use L<Plack::Middleware::Basic::Auth> instead.
70              
71             =head2 Advanced Error Handling
72              
73             You can also throw error messages rather than just C<die>ing, which will throw an internal server error. To throw a specific type of error, C<die>, C<carp>, or C<confess>, an array reference starting with the error code, then the error message, and finally ending with error data (optional). When JSON::RPC::Dispatcher detects this, it will throw that specific error message rather than a standard internal server error.
74              
75             use JSON::RPC::Dispatcher;
76             my $rpc = JSON::RPC::Dispatcher->new;
77              
78             sub guess {
79             my ($guess) = @_;
80             if ($guess == 10) {
81             return 'Correct!';
82             }
83             elsif ($guess > 10) {
84             die [986, 'Too high.'];
85             }
86             else {
87             die [987, 'Too low.'];
88             }
89             }
90              
91             $rpc->register( 'guess', \&guess );
92              
93             $rpc->to_app;
94              
95             B<NOTE:> If you don't care about setting error codes and just want to set an error message, you can simply C<die> in your RPC and your die message will be inserted into the C<error_data> method.
96              
97             =head2 Logging
98              
99             JSON::RPC::Dispatcher allows for logging via L<Log::Any>. This way you can set up logs with L<Log::Dispatch>, L<Log::Log4perl>, or any other logging system that L<Log::Any> supports now or in the future. It's relatively easy to set up. In your F<app.psgi> simply add a block like this:
100              
101             use Log::Any::Adapter;
102             use Log::Log4perl;
103             Log::Log4perl::init('/path/to/log4perl.conf');
104             Log::Any::Adapter->set('Log::Log4perl');
105              
106             That's how easy it is to start logging. You'll of course still need to configure the F<log4perl.conf> file, which goes well beyond the scope of this document. And you'll also need to install L<Log::Any::Adapter::Log4perl> to use this example.
107              
108             JSON::RPC::Dispatcher logs the following:
109              
110             =over
111              
112             =item INFO
113              
114             Requests and responses.
115              
116             =item DEBUG
117              
118             In the case when there is an unhandled exception, anything other than the error message will be put into a debug log entry.
119              
120             =item TRACE
121              
122             If an exception is thrown that has a C<trace> method, then it's contents will be put into a trace log entry.
123              
124             =item ERROR
125              
126             All errors that are gracefully handled by the system will be put into an error log entry.
127              
128             =item FATAL
129              
130             All errors that are not gracefully handled by the system will be put into a fatal log entry. Most of the time this means there's something wrong with the request document itself.
131              
132             =back
133              
134             =cut
135              
136              
137 1     1   510 use Moose;
  0            
  0            
138             use utf8;
139             extends qw(Plack::Component);
140             use Plack::Request;
141             use JSON;
142             use JSON::RPC::Dispatcher::Procedure;
143             use Log::Any qw($log);
144              
145             #--------------------------------------------------------
146             has error_code => (
147             is => 'rw',
148             default => undef,
149             predicate => 'has_error_code',
150             clearer => 'clear_error_code',
151             );
152              
153             #--------------------------------------------------------
154             has error_message => (
155             is => 'rw',
156             default => undef,
157             clearer => 'clear_error_message',
158             );
159              
160             #--------------------------------------------------------
161             has error_data => (
162             is => 'rw',
163             default => undef,
164             clearer => 'clear_error_data',
165             );
166              
167             #--------------------------------------------------------
168             has rpcs => (
169             is => 'rw',
170             default => sub { {} },
171             );
172              
173             #--------------------------------------------------------
174             sub clear_error {
175             my ($self) = @_;
176              
177             $self->clear_error_code;
178             $self->clear_error_message;
179             $self->clear_error_data;
180             }
181              
182             #--------------------------------------------------------
183             sub register {
184             my ($self, $name, $sub, $options) = @_;
185             my $rpcs = $self->rpcs;
186             $rpcs->{$name} = {
187             function => $sub,
188             with_plack_request => $options->{with_plack_request},
189             };
190             $self->rpcs($rpcs);
191             }
192              
193             #--------------------------------------------------------
194             sub acquire_procedures {
195             my ($self, $request) = @_;
196             if ($request->method eq 'POST') {
197             return $self->acquire_procedures_from_post($request);
198             }
199             elsif ($request->method eq 'GET') {
200             return [ $self->acquire_procedure_from_get($request) ];
201             }
202             else {
203             $self->error_code(-32600);
204             $self->error_message('Invalid Request.');
205             $self->error_data('Invalid method type: '.$request->method);
206             return [];
207             }
208             }
209              
210             #--------------------------------------------------------
211             sub acquire_procedures_from_post {
212             my ($self, $plack_request) = @_;
213             my $body = $plack_request->content;
214             my $request = eval{from_json($body, {utf8=>1})};
215             if ($@) {
216             $self->error_code(-32700);
217             $self->error_message('Parse error.');
218             $self->error_data($body);
219             $log->fatal('Parse error.');
220             $log->debug($body);
221             return undef;
222             }
223             else {
224             if (ref $request eq 'ARRAY') {
225             my @procs;
226             foreach my $proc (@{$request}) {
227             push @procs, $self->create_proc($proc->{method}, $proc->{id}, $proc->{params}, $plack_request);
228             }
229             return \@procs;
230             }
231             elsif (ref $request eq 'HASH') {
232             return [ $self->create_proc($request->{method}, $request->{id}, $request->{params}, $plack_request) ];
233             }
234             else {
235             $self->error_code(-32600);
236             $self->error_message('Invalid request.');
237             $self->error_data($request);
238             $log->fatal('Invalid request.');
239             $log->debug($body);
240             return undef;
241             }
242             }
243             }
244              
245             #--------------------------------------------------------
246             sub acquire_procedure_from_get {
247             my ($self, $plack_request) = @_;
248             my $params = $plack_request->query_parameters;
249             my $decoded_params = (exists $params->{params}) ? eval{from_json($params->{params},{utf8=>1})} : undef;
250             return $self->create_proc($params->{method}, $params->{id}, ($@ || $decoded_params), $plack_request);
251             }
252              
253             #--------------------------------------------------------
254             sub create_proc {
255             my ($self, $method, $id, $params, $plack_request) = @_;
256             my $proc = JSON::RPC::Dispatcher::Procedure->new(
257             method => $method,
258             id => $id,
259             );
260              
261             # process parameters
262             if (defined $params) {
263             unless (ref $params eq 'ARRAY' or ref $params eq 'HASH') {
264             $proc->invalid_params($params);
265             return $proc;
266             }
267             }
268             my @vetted;
269             if (ref $params eq 'HASH') {
270             @vetted = (%{$params});
271             }
272             elsif (ref $params eq 'ARRAY') {
273             @vetted = (@{$params});
274             }
275             if ($self->rpcs->{$proc->method}{with_plack_request}) {
276             unshift @vetted, $plack_request;
277             }
278             $proc->params(\@vetted);
279             return $proc;
280             }
281              
282             #--------------------------------------------------------
283             sub translate_error_code_to_status {
284             my ($self, $code) = @_;
285             $code ||= '';
286             my %trans = (
287             '' => 200,
288             '-32600' => 400,
289             '-32601' => 404,
290             );
291             my $status = $trans{$code};
292             $status ||= 500;
293             return $status;
294             }
295              
296             #--------------------------------------------------------
297             sub handle_procedures {
298             my ($self, $procs) = @_;
299             my @responses;
300             my $rpcs = $self->rpcs;
301             foreach my $proc (@{$procs}) {
302             my $is_notification = (defined $proc->id && $proc->id ne '') ? 0 : 1;
303             unless ($proc->has_error_code) {
304             my $rpc = $rpcs->{$proc->method};
305             my $code_ref = $rpc->{function};
306             if (defined $code_ref) {
307             # deal with params and calling
308             my $result = eval{ $code_ref->( @{ $proc->params } ) };
309              
310             # deal with result
311             if ($@ && ref($@) eq 'Ouch') {
312             $proc->error($@->code, $@->message, $@->data);
313             $log->error($@->message);
314             $log->debug($@->data);
315             }
316             elsif ($@ && ref($@) eq 'ARRAY') {
317             $proc->error(@{$@});
318             $log->error($@->[1]);
319             $log->debug($@->[2]);
320             }
321             elsif ($@) {
322             my $error = $@;
323             if ($error->can('error') && $error->can('trace')) {
324             $log->fatal($error->error);
325             $log->trace($error->trace->as_string);
326             $error = $error->error;
327             }
328             elsif ($error->can('error')) {
329             $error = $error->error;
330             $log->fatal($error);
331             }
332             elsif (ref $error ne '' && ref $error ne 'HASH' && ref $error ne 'ARRAY') {
333             $log->fatal($error);
334             $error = ref $error;
335             }
336             $proc->internal_error($error);
337             }
338             else {
339             $proc->result($result);
340             }
341             }
342             else {
343             $proc->method_not_found($proc->method);
344             }
345             }
346              
347             # remove not needed elements per section 5 of the spec
348             my $response = $proc->response;
349             if (exists $response->{error}{code}) {
350             delete $response->{result};
351             }
352             else {
353             delete $response->{error};
354             }
355              
356             # remove responses on notifications per section 4.1 of the spec
357             unless ($is_notification) {
358             push @responses, $response;
359             }
360             }
361              
362             # return the appropriate response, for batch or not
363             if (scalar(@responses) > 1) {
364             return \@responses;
365             }
366             else {
367             return $responses[0];
368             }
369             }
370              
371             #--------------------------------------------------------
372             sub call {
373             my ($self, $env) = @_;
374              
375             my $request = Plack::Request->new($env);
376             $log->info("REQUEST: ".$request->content) if $log->is_info;
377             $self->clear_error;
378             my $procs = $self->acquire_procedures($request);
379              
380             my $rpc_response;
381             if ($self->has_error_code) {
382             $rpc_response = {
383             jsonrpc => '2.0',
384             error => {
385             code => $self->error_code,
386             message => $self->error_message,
387             data => $self->error_data,
388             },
389             };
390             }
391             else {
392             $rpc_response = $self->handle_procedures($procs);
393             }
394              
395             my $response = $request->new_response;
396             if ($rpc_response) {
397             my $json = eval{JSON->new->utf8->encode($rpc_response)};
398             if ($@) {
399             $log->error("JSON repsonse error: ".$@);
400             $json = JSON->new->utf8->encode({
401             jsonrpc => "2.0",
402             error => {
403             code => -32099,
404             message => "Couldn't convert method response to JSON.",
405             data => $@,
406             }
407             });
408             }
409             $response->status($self->translate_error_code_to_status( (ref $rpc_response eq 'HASH' && exists $rpc_response->{error}) ? $rpc_response->{error}{code} : '' ));
410             $response->content_type('application/json-rpc');
411             $response->content_length(bytes::length($json));
412             $response->body($json);
413             if ($response->status == 200) {
414             $log->info("RESPONSE: ".$response->body) if $log->is_info;
415             }
416             else {
417             $log->error("RESPONSE: ".$response->body);
418             }
419             }
420             else { # is a notification only request
421             $response->status(204);
422             $log->info('RESPONSE: Notification Only');
423             }
424             return $response->finalize;
425             }
426              
427             =head1 PREREQS
428              
429             L<Moose>
430             L<JSON>
431             L<Plack>
432             L<Test::More>
433             L<Log::Any>
434              
435             =head1 SUPPORT
436              
437             =over
438              
439             =item Repository
440              
441             L<http://github.com/plainblack/JSON-RPC-Dispatcher>
442              
443             =item Bug Reports
444              
445             L<http://github.com/plainblack/JSON-RPC-Dispatcher/issues>
446              
447             =back
448              
449             =head1 SEE ALSO
450              
451             You may also want to check out these other modules, especially if you're looking for something that works with JSON-RPC 1.x.
452              
453             =over
454              
455             =item Dispatchers
456              
457             Other modules that compete directly with this module, though perhaps on other protocol versions.
458              
459             =over
460              
461             =item L<JSON::RPC>
462              
463             An excellent and fully featured both client and server for JSON-RPC 1.1.
464              
465             =item L<POE::Component::Server::JSONRPC>
466              
467             A JSON-RPC 1.0 server for POE. I couldn't get it to work, and it doesn't look like it's maintained.
468              
469             =item L<Catalyst::Plugin::Server::JSONRPC>
470              
471             A JSON-RPC 1.1 dispatcher for Catalyst.
472              
473             =item L<CGI-JSONRPC>
474              
475             A CGI/Apache based JSON-RPC 1.1 dispatcher. Looks to be abandoned in alpha state. Also includes L<Apache2::JSONRPC>.
476              
477             =item L<AnyEvent::JSONRPC::Lite>
478              
479             An L<AnyEvent> JSON-RPC 1.x dispatcher.
480              
481             =item L<Sledge::Plugin::JSONRPC>
482              
483             JSON-RPC 1.0 dispatcher for Sledge MVC framework.
484              
485             =back
486              
487             =item Clients
488              
489             Modules that you'd use to access various dispatchers.
490              
491             =over
492              
493             =item L<JSON::RPC::Common>
494              
495             A JSON-RPC client for 1.0, 1.1, and 2.0. Haven't used it, but looks pretty feature complete.
496              
497             =item L<RPC::JSON>
498              
499             A simple and good looking JSON::RPC 1.x client. I haven't tried it though.
500              
501             =back
502              
503             =back
504              
505             =head1 AUTHOR
506              
507             JT Smith <jt_at_plainblack_com>
508              
509             =head1 LEGAL
510              
511             JSON::RPC::Dispatcher is Copyright 2009-2010 Plain Black Corporation (L<http://www.plainblack.com/>) and is licensed under the same terms as Perl itself.
512              
513             =cut
514              
515             1;