File Coverage

blib/lib/JSON/RPC/Dispatcher.pm
Criterion Covered Total %
statement 25 159 15.7
branch 4 64 6.2
condition 9 43 20.9
subroutine 7 15 46.6
pod 1 9 11.1
total 46 290 15.8


line stmt bran cond sub pod time code
1             package JSON::RPC::Dispatcher;
2             $JSON::RPC::Dispatcher::VERSION = '0.0508';
3             =head1 NAME
4              
5             JSON::RPC::Dispatcher - A JSON-RPC 2.0 server.
6              
7             =head1 VERSION
8              
9             version 0.0508
10              
11             =head1 SYNOPSIS
12              
13             In F<app.psgi>:
14              
15             use JSON::RPC::Dispatcher;
16              
17             my $rpc = JSON::RPC::Dispatcher->new;
18              
19             sub add_em {
20             my @params = @_;
21             my $sum = 0;
22             $sum += $_ for @params;
23             return $sum;
24             }
25             $rpc->register( 'sum', \&add_em );
26              
27             $rpc->to_app;
28              
29             Then run it:
30              
31             plackup app.psgi
32              
33             Now you can then call this service via a GET like:
34              
35             http://example.com/?method=sum;params=[2,3,5];id=1
36              
37             Or by posting JSON to it like this:
38              
39             {"jsonrpc":"2.0","method":"sum","params":[2,3,5],"id":"1"}
40              
41             And you'd get back:
42              
43             {"jsonrpc":"2.0","result":10,"id":"1"}
44            
45             =head1 DESCRIPTION
46              
47             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.
48              
49             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>.
50              
51             =head2 Registration Options
52              
53             The C<register> method cannot be used to register methods that start with m/^rpc\./. Per the JSON-RPC 2.0 specification, these are reserved for
54             rpc-internal extensions.
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             =head3 log_request_as
72              
73             This is a filter function for manipulating the parameters before being logged. This is especially useful for code that accepts passwords.
74              
75             The first parameter to the code ref here will be the method name, the second is the parameter array reference. The code ref is expected to return the modified param, but be careful.
76             The array ref being passed in has had the plack_request removed, and so the array ref is a copy of the one that will be eventually passed to the handler function, so modifying the
77             array is safe. However, if an element of the array is another reference, that is not a copy, and so modifying that will require extra care.
78              
79             sub {
80             my ($method, $params) = @_;
81             $params->[1] = 'xxx'; # works
82             $params->[0]{password} = 'xxx'; # broken
83             $params->[0] = { %{$params->[0]}, password => 'xxx' }; # works.
84              
85             return $params; # required
86             }
87              
88             =head2 Advanced Error Handling
89              
90             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.
91              
92             use JSON::RPC::Dispatcher;
93             my $rpc = JSON::RPC::Dispatcher->new;
94              
95             sub guess {
96             my ($guess) = @_;
97             if ($guess == 10) {
98             return 'Correct!';
99             }
100             elsif ($guess > 10) {
101             die [986, 'Too high.'];
102             }
103             else {
104             die [987, 'Too low.'];
105             }
106             }
107              
108             $rpc->register( 'guess', \&guess );
109              
110             $rpc->to_app;
111              
112             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.
113              
114             =head2 Logging
115              
116             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:
117              
118             use Log::Any::Adapter;
119             use Log::Log4perl;
120             Log::Log4perl::init('/path/to/log4perl.conf');
121             Log::Any::Adapter->set('Log::Log4perl');
122              
123             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.
124              
125             JSON::RPC::Dispatcher logs the following:
126              
127             =over
128              
129             =item INFO
130              
131             Requests and responses.
132              
133             =item DEBUG
134              
135             In the case when there is an unhandled exception, anything other than the error message will be put into a debug log entry.
136              
137             =item TRACE
138              
139             If an exception is thrown that has a C<trace> method, then its contents will be put into a trace log entry.
140              
141             =item ERROR
142              
143             All errors that are gracefully handled by the system will be put into an error log entry.
144              
145             =item FATAL
146              
147             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.
148              
149             =back
150              
151             =cut
152              
153              
154 3     3   25493 use Moose;
  3         2897933  
  3         20  
155             extends qw(Plack::Component);
156 3     3   23182 use Plack::Request;
  3         228446  
  3         105  
157 3     3   1083 use JSON;
  3         13073  
  3         22  
158 3     3   2380 use JSON::RPC::Dispatcher::Procedure;
  3         54264  
  3         170  
159 3     3   2824 use Log::Any qw($log);
  3         41752  
  3         15  
160 3     3   14564 use Scalar::Util qw(blessed);
  3         7  
  3         5766  
161              
162             #--------------------------------------------------------
163             has error_code => (
164             is => 'rw',
165             default => undef,
166             predicate => 'has_error_code',
167             clearer => 'clear_error_code',
168             );
169              
170             #--------------------------------------------------------
171             has error_message => (
172             is => 'rw',
173             default => undef,
174             clearer => 'clear_error_message',
175             );
176              
177             #--------------------------------------------------------
178             has error_data => (
179             is => 'rw',
180             default => undef,
181             clearer => 'clear_error_data',
182             );
183              
184             #--------------------------------------------------------
185             has rpcs => (
186             is => 'rw',
187             default => sub { {} },
188             );
189              
190             #--------------------------------------------------------
191             sub clear_error {
192 0     0 0 0 my ($self) = @_;
193              
194 0         0 $self->clear_error_code;
195 0         0 $self->clear_error_message;
196 0         0 $self->clear_error_data;
197             }
198              
199             #--------------------------------------------------------
200             sub register {
201 18     18 0 5144 my ($self, $name, $sub, $options) = @_;
202              
203 18 100 100     183 if(defined($name) && $name =~ m{^rpc\.}) {
    100 100        
      100        
204 2         19 die "$name is an invalid name for a method. (Methods matching m/^rpc\\./ are reserved for rpc-internal procedures)";
205             } elsif(!defined($name) || $name eq '' || ref($name)) {
206 4         25 die "Registered method name must be a defined non-empty string and not start with 'rpc.'";
207             }
208            
209 12         480 my $rpcs = $self->rpcs;
210             $rpcs->{$name} = {
211             function => $sub,
212             with_plack_request => $options->{with_plack_request},
213             log_request_as => $options->{log_request_as},
214 12         55 };
215 12         470 $self->rpcs($rpcs);
216             }
217              
218             #--------------------------------------------------------
219             sub acquire_procedures {
220 0     0 0   my ($self, $request) = @_;
221 0 0         if ($request->method eq 'POST') {
    0          
222 0           return $self->acquire_procedures_from_post($request);
223             }
224             elsif ($request->method eq 'GET') {
225 0           return [ $self->acquire_procedure_from_get($request) ];
226             }
227             else {
228 0           $self->error_code(-32600);
229 0           $self->error_message('Invalid Request.');
230 0           $self->error_data('Invalid method type: '.$request->method);
231 0           return [];
232             }
233             }
234              
235             #--------------------------------------------------------
236             sub acquire_procedures_from_post {
237 0     0 0   my ($self, $plack_request) = @_;
238 0           my $body = $plack_request->content;
239 0           my $request = eval{from_json($body, {utf8=>1})};
  0            
240 0 0         if ($@) {
241 0           $self->error_code(-32700);
242 0           $self->error_message('Parse error.');
243 0           $self->error_data($body);
244 0           $log->fatal('Parse error.');
245 0           $log->debug($body);
246 0           return undef;
247             }
248             else {
249 0 0         if (ref $request eq 'ARRAY') {
    0          
250 0           my @procs;
251 0           foreach my $proc (@{$request}) {
  0            
252 0           push @procs, $self->create_proc($proc->{method}, $proc->{id}, $proc->{params}, $plack_request);
253             }
254 0           return \@procs;
255             }
256             elsif (ref $request eq 'HASH') {
257 0           return [ $self->create_proc($request->{method}, $request->{id}, $request->{params}, $plack_request) ];
258             }
259             else {
260 0           $self->error_code(-32600);
261 0           $self->error_message('Invalid request.');
262 0           $self->error_data($request);
263 0           $log->fatal('Invalid request.');
264 0           $log->debug($body);
265 0           return undef;
266             }
267             }
268             }
269              
270             #--------------------------------------------------------
271             sub acquire_procedure_from_get {
272 0     0 0   my ($self, $plack_request) = @_;
273 0           my $params = $plack_request->query_parameters;
274 0 0         my $decoded_params = (exists $params->{params}) ? eval{from_json($params->{params},{utf8=>1})} : undef;
  0            
275 0   0       return $self->create_proc($params->{method}, $params->{id}, ($@ || $decoded_params), $plack_request);
276             }
277              
278             #--------------------------------------------------------
279             sub create_proc {
280 0     0 0   my ($self, $method, $id, $params, $plack_request) = @_;
281 0           my $proc = JSON::RPC::Dispatcher::Procedure->new(
282             method => $method,
283             id => $id,
284             );
285              
286             # process parameters
287 0 0         if (defined $params) {
288 0 0 0       unless (ref $params eq 'ARRAY' or ref $params eq 'HASH') {
289 0           $proc->invalid_params($params);
290 0           return $proc;
291             }
292             }
293 0           my @vetted;
294 0 0         if (ref $params eq 'HASH') {
    0          
295 0           @vetted = (%{$params});
  0            
296             }
297             elsif (ref $params eq 'ARRAY') {
298 0           @vetted = (@{$params});
  0            
299             }
300 0 0         if ($self->rpcs->{$proc->method}{with_plack_request}) {
301 0           unshift @vetted, $plack_request;
302             }
303 0           $proc->params(\@vetted);
304 0           return $proc;
305             }
306              
307             #--------------------------------------------------------
308             sub translate_error_code_to_status {
309 0     0 0   my ($self, $code) = @_;
310 0   0       $code ||= '';
311 0           my %trans = (
312             '' => 200,
313             '-32600' => 400,
314             '-32601' => 404,
315             );
316 0           my $status = $trans{$code};
317 0   0       $status ||= 500;
318 0           return $status;
319             }
320              
321             #--------------------------------------------------------
322             sub handle_procedures {
323 0     0 0   my ($self, $procs) = @_;
324 0           my @responses;
325 0           my $rpcs = $self->rpcs;
326 0           foreach my $proc (@{$procs}) {
  0            
327 0 0 0       my $is_notification = (defined $proc->id && $proc->id ne '') ? 0 : 1;
328 0 0         unless ($proc->has_error_code) {
329 0           my $rpc = $rpcs->{$proc->method};
330 0           my $code_ref = $rpc->{function};
331 0 0         if (defined $code_ref) {
332             # deal with params and calling
333 0 0         if ($log->is_info) {
334 0           my $params = [grep { ! blessed $_ } @{$proc->params} ];
  0            
  0            
335 0 0         if (my $func = $self->rpcs->{$proc->method}{log_request_as}) {
336 0           $params = $func->($proc->method, $params);
337             }
338 0           $log->info("REQUEST: " . $proc->method . " " . to_json( $params ));
339             }
340 0           my $result = eval{ $code_ref->( @{ $proc->params } ) };
  0            
  0            
341              
342             # deal with result
343 0 0 0       if ($@ && ref($@) eq 'ARRAY') {
    0          
344 0           $proc->error(@{$@});
  0            
345 0           $log->error($@->[1]);
346 0           $log->debug($@->[2]);
347             }
348             elsif ($@) {
349 0           my $error = $@;
350 0 0 0       if (blessed($error) && $error->can('error') && $error->can('trace')) {
    0 0        
    0 0        
      0        
      0        
351 0           $log->fatal($error->error);
352 0           $log->trace($error->trace->as_string);
353 0           $error = $error->error;
354             }
355             elsif (blessed($error) && $error->can('error')) {
356 0           $error = $error->error;
357 0           $log->fatal($error);
358             }
359             elsif (ref $error ne '' && ref $error ne 'HASH' && ref $error ne 'ARRAY') {
360 0           $log->fatal($error);
361 0           $error = ref $error;
362             }
363 0           $proc->internal_error($error);
364             }
365             else {
366 0           $proc->result($result);
367             }
368             }
369             else {
370 0           $proc->method_not_found($proc->method);
371             }
372             }
373              
374             # remove not needed elements per section 5 of the spec
375 0           my $response = $proc->response;
376 0 0         if (exists $response->{error}{code}) {
377 0           delete $response->{result};
378             }
379             else {
380 0           delete $response->{error};
381             }
382              
383             # remove responses on notifications per section 4.1 of the spec
384 0 0         unless ($is_notification) {
385 0           push @responses, $response;
386             }
387             }
388              
389             # return the appropriate response, for batch or not
390 0 0         if (scalar(@responses) > 1) {
391 0           return \@responses;
392             }
393             else {
394 0           return $responses[0];
395             }
396             }
397              
398             #--------------------------------------------------------
399             sub call {
400 0     0 1   my ($self, $env) = @_;
401              
402 0           my $request = Plack::Request->new($env);
403 0           $self->clear_error;
404 0           my $procs = $self->acquire_procedures($request);
405              
406 0           my $rpc_response;
407 0 0         if ($self->has_error_code) {
408 0           $rpc_response = {
409             jsonrpc => '2.0',
410             error => {
411             code => $self->error_code,
412             message => $self->error_message,
413             data => $self->error_data,
414             },
415             };
416             }
417             else {
418 0           $rpc_response = $self->handle_procedures($procs);
419             }
420              
421 0           my $response = $request->new_response;
422 0 0         if ($rpc_response) {
423 0           my $json = eval{JSON->new->utf8->encode($rpc_response)};
  0            
424 0 0         if ($@) {
425 0           $log->error("JSON repsonse error: ".$@);
426 0           $json = JSON->new->utf8->encode({
427             jsonrpc => "2.0",
428             error => {
429             code => -32099,
430             message => "Couldn't convert method response to JSON.",
431             data => $@,
432             }
433             });
434             }
435 0 0 0       $response->status($self->translate_error_code_to_status( (ref $rpc_response eq 'HASH' && exists $rpc_response->{error}) ? $rpc_response->{error}{code} : '' ));
436 0           $response->content_type('application/json-rpc');
437 0           $response->content_length(length($json));
438 0           $response->body($json);
439 0 0         if ($response->status == 200) {
440 0 0         $log->info("RESPONSE: ".$response->body) if $log->is_info;
441             }
442             else {
443 0           $log->error("RESPONSE: ".$response->body);
444             }
445             }
446             else { # is a notification only request
447 0           $response->status(204);
448 0           $log->info('RESPONSE: Notification Only');
449             }
450 0           return $response->finalize;
451             }
452              
453             =head1 PREREQS
454              
455             L<Moose>
456             L<JSON>
457             L<Plack>
458             L<Test::More>
459             L<Log::Any>
460              
461             =head1 SUPPORT
462              
463             =over
464              
465             =item Repository
466              
467             L<http://github.com/plainblack/JSON-RPC-Dispatcher>
468              
469             =item Bug Reports
470              
471             L<http://github.com/plainblack/JSON-RPC-Dispatcher/issues>
472              
473             =back
474              
475             =head1 SEE ALSO
476              
477             You may also want to check out these other modules, especially if you're looking for something that works with JSON-RPC 1.x.
478              
479             =over
480              
481             =item Dispatchers
482              
483             Other modules that compete directly with this module, though perhaps on other protocol versions.
484              
485             =over
486              
487             =item L<JSON::RPC>
488              
489             An excellent and fully featured both client and server for JSON-RPC 1.1.
490              
491             =item L<POE::Component::Server::JSONRPC>
492              
493             A JSON-RPC 1.0 server for POE. I couldn't get it to work, and it doesn't look like it's maintained.
494              
495             =item L<Catalyst::Plugin::Server::JSONRPC>
496              
497             A JSON-RPC 1.1 dispatcher for Catalyst.
498              
499             =item L<CGI-JSONRPC>
500              
501             A CGI/Apache based JSON-RPC 1.1 dispatcher. Looks to be abandoned in alpha state. Also includes L<Apache2::JSONRPC>.
502              
503             =item L<AnyEvent::JSONRPC::Lite>
504              
505             An L<AnyEvent> JSON-RPC 1.x dispatcher.
506              
507             =item L<Sledge::Plugin::JSONRPC>
508              
509             JSON-RPC 1.0 dispatcher for Sledge MVC framework.
510              
511             =back
512              
513             =item Clients
514              
515             Modules that you'd use to access various dispatchers.
516              
517             =over
518              
519             =item L<JSON::RPC::Common>
520              
521             A JSON-RPC client for 1.0, 1.1, and 2.0. Haven't used it, but looks pretty feature complete.
522              
523             =item L<RPC::JSON>
524              
525             A simple and good looking JSON::RPC 1.x client. I haven't tried it though.
526              
527             =back
528              
529             =back
530              
531             =head1 AUTHOR
532              
533             JT Smith <jt_at_plainblack_com>
534              
535             =head1 LEGAL
536              
537             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.
538              
539             =cut
540              
541             1;