File Coverage

blib/lib/JSON/RPC/Dispatcher.pm
Criterion Covered Total %
statement 18 156 11.5
branch 0 60 0.0
condition 0 28 0.0
subroutine 6 15 40.0
pod 1 9 11.1
total 25 268 9.3


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