File Coverage

blib/lib/JSON/RPC/Simple/Dispatcher.pm
Criterion Covered Total %
statement 114 152 75.0
branch 40 74 54.0
condition 10 23 43.4
subroutine 16 18 88.8
pod 6 10 60.0
total 186 277 67.1


line stmt bran cond sub pod time code
1             package JSON::RPC::Simple::Dispatcher;
2              
3 1     1   897 use strict;
  1         2  
  1         37  
4 1     1   5 use warnings;
  1         3  
  1         27  
5              
6 1     1   6 use Carp qw(croak);
  1         2  
  1         44  
7 1     1   2616 use HTTP::Response;
  1         11295  
  1         41  
8 1     1   11 use JSON qw();
  1         3  
  1         2033  
9              
10             sub new {
11 1     1 1 3 my ($pkg, $args) = @_;
12              
13 1 50       4 $args = {} unless ref $args eq "HASH";
14            
15 1         25 my $self = bless {
16             charset => "UTF-8",
17             json => JSON->new->utf8,
18             error_handler => undef,
19             %$args,
20             target => {}
21             }, $pkg;
22 1         4 return $self;
23             }
24              
25             sub json {
26 12     12 1 16 my $self = shift;
27 12 50       90 $self->{json} = shift if @_;
28 12         149 return $self->{json};
29             }
30              
31             sub error_handler {
32 3     3 1 5 my $self = shift;
33 3 50       10 $self->{error_handler} = shift if @_;
34 3         12 return $self->{error_handler};
35             }
36              
37             sub charset {
38 6     6 1 10 my $self = shift;
39 6 50       17 $self->{charset} = shift if @_;
40 6         35 return $self->{charset};
41             }
42              
43             sub dispatch_to {
44 3     3 1 2165 my ($self, $targets) = @_;
45            
46 3 50       13 croak "Targets is not hash reference" unless ref $targets eq "HASH";
47              
48 3         14 while (my ($path, $target) = each %$targets) {
49 2 100       19 unless ($target->isa("JSON::RPC::Simple")) {
50 1         28 croak qq{Target for "${path}" is not a JSON::RPC::Simple};
51             }
52 1         26 $self->{target}->{$path} = $target;
53             }
54            
55 2         9 return $self;
56             }
57              
58 3     3 0 7 sub JSONRPC_ERROR { undef; }
59              
60             our $HTTP_ERROR_CODE;
61             sub _error {
62 3     3   8 my ($self, $request, $id, $code, $message, $error_obj, $call, $target) = @_;
63              
64 3 50       7 $message = "Uknown error" unless defined $message;
65            
66 3 50       26 my $error = {
67             (defined $id ? (id => $id) : ()),
68             version => "1.1",
69             error => {
70             name => "JSONRPCError",
71             code => int($code),
72             message => $message,
73             },
74             };
75            
76 3 50       8 if ($error_obj) {
77 0         0 $error->{error}->{error} = $error_obj;
78             }
79             else {
80             # No error object provided
81             # Here, if there's a error callback handler registered on the
82             # target first user that, secondly check if there's an
83             # error handler on the dispatcher
84 3         4 my $new_error_obj;
85 3 50 33     34 if ($target && $target->can("JSONRPC_ERROR")) {
86 0         0 $new_error_obj = $target->JSONRPC_ERROR(
87             $request, $code, $message, $call
88             );
89             }
90 3 50       13 $new_error_obj = $self->JSONRPC_ERROR unless $new_error_obj;
91 3 50 33     13 if ($self->error_handler && !$new_error_obj) {
92 0         0 $new_error_obj = $self->error_handler->(
93             $request, $code, $message, $call, $target
94             );
95             }
96              
97 3 50       9 $error->{error}->{error} = $new_error_obj if $new_error_obj;
98             }
99            
100 3 100       10 my $status_line = $HTTP_ERROR_CODE == 200 ? "OK" : "Internal Server Error";
101 3         9 return $self->_encode_response($HTTP_ERROR_CODE, $status_line, $error);
102             }
103              
104             sub _encode_response {
105 6     6   12 my ($self, $code, $message, $response) = @_;
106            
107 6         17 my $content = $self->json->encode($response);
108 6         36 my $h = HTTP::Headers->new();
109 6         67 $h->header("Content-Type" => "application/json; charset=" . $self->charset);
110 6         253 $h->header("Content-Length" => length $content);
111            
112 6         207 return HTTP::Response->new($code, $message, $h, $content);
113             }
114              
115             sub errstr {
116 3   50 3 0 851 return shift->{errstr} || "";
117             }
118              
119             sub errobj {
120 0     0 0 0 return shift->{errobj};
121             }
122              
123             sub handle {
124 6     6 1 16130 my ($self, $path, $request) = @_;
125            
126 6         11 $HTTP_ERROR_CODE = 500;
127            
128             # Clear any previous errors
129 6         12 delete $self->{errstr};
130            
131             # Don't support GET or other methods
132 6 50       22 unless ($request->method eq "POST") {
133 0         0 $self->{errstr} = "I only do POST";
134 0         0 return $self->_error($request, undef, 0, $self->errstr);
135             }
136            
137 6 50       103 unless ($request->content_type =~ m{^application/json}) {
138 0         0 $self->{errstr} =
139             "Invalid Content-Type, got '" . $request->content_type . "'";
140 0         0 return $self->_error($request, undef, 0, $self->errstr);
141             }
142              
143             # Some requests, like HTTP::Request lazy load content_length so we can't ->can("") it which is why the eval
144 6         239 my $content_length = eval { $request->content_length };
  6         32  
145 6 50       200 if ($@) {
146             # Apache2::RequestReq
147 0 0       0 $content_length = $request->headers_in->{'Content-Length'} if $request->can("headers_in");
148            
149             # Fallback
150 0 0 0     0 $content_length = $request->headers->{'Content-Length'} if !defined $content_length && $request->can("headers");
151             };
152            
153 6 50       16 unless (defined $content_length) {
154 0         0 $self->{errstr} =
155             "JSON-RPC 1.1 requires header Content-Length to be specified";
156 0         0 return $self->_error($request, undef, 0, $self->errstr);
157             }
158            
159             # Find target
160 6         17 my $target = $self->{target}->{$path};
161            
162             # Decode the call and trap errors because it might
163             # be invalid JSON
164 6         8 my $call;
165 6         8 eval {
166 6         29 my $content = $request->content;
167              
168             # Remove utf-8 BOM if present
169 6         74 $content =~ s/^(?:\xef\xbb\xbf|\xfe\xff|\xff\xfe)//;
170            
171 6         17 $call = $self->json->decode($content);
172             };
173 6 50       17 if ($@) {
174 0         0 $self->{errstr} = "$@";
175 0         0 $self->{errobj} = $@;
176 0         0 return $self->_error(
177             $request, undef, 0, $self->errstr, undef, undef, $target
178             );
179             }
180            
181 6         10 my $id = $call->{id};
182 6         9 my $version = $call->{version};
183 6 50       18 unless (defined $version) {
184 0         0 $self->{errstr} = "Missing 'version'";
185 0         0 return $self->_error(
186             $request, $id, 0, $self->errstr, undef, $call, $target
187             );
188             }
189 6 50       15 unless ($version eq "1.1") {
190 0         0 $self->{errstr} = "I only do JSON-RPC 1.1";
191 0         0 return $self->_error(
192             $request, $id, 0, $self->errstr, undef, $call, $target
193             );
194             }
195            
196 6         13 my $method = $call->{method};
197 6 50       14 unless ($method) {
198 0         0 $self->{errstr} = "Missing method";
199 0         0 $self->_error($request, $id, 0, $self->errstr, undef, $call, $target);
200             }
201            
202            
203 6         10 my $params = $call->{params};
204 6 50       15 unless ($params) {
205 0         0 $self->_error($id, 0, $self->errstr, undef, $call, $target);
206             }
207              
208 6 50 66     23 unless (ref $params eq "HASH" || ref $params eq "ARRAY") {
209 0         0 $self->{errstr} = "Invalid params, expecting object or array";
210 0         0 return $self->_error(
211             $request, $id, 0, $self->errstr, undef, $call, $target
212             );
213             }
214              
215 6 50       17 unless ($target) {
216 0         0 $self->{errstr} = "No target for '${path}' exists";
217 0         0 return $self->_error(
218             $request, $id, 0, $self->errstr, undef, $call, $target
219             );
220             }
221            
222 6         52 my $cv = $target->can($method);
223 6         9 my $check_attrs;
224 6 100       15 if ($cv) {
225             # Check that it's a JSONRpcMethod
226 5         32 my @attrs = JSON::RPC::Simple->fetch_method_arguments($cv);
227 5 50       14 unless (@attrs) {
228 0         0 $self->{errstr} = "Procedure not found";
229 0         0 return $self->_error(
230             $request, $id, 0, $self->errstr, undef, $call, $target
231             );
232             }
233 5         13 $check_attrs = shift @attrs;
234             }
235             else {
236             # Check for fallback
237 1 50       11 if ($cv = $target->can("JSONRPC_AUTOLOAD")) {
238 1   33     6 my $pkg = ref $target || $target;
239 1     1   8 no strict 'refs';
  1         95  
  1         563  
240 1         3 ${"${pkg}::JSONRPC_AUTOLOAD"} = $method;
  1         4  
241            
242 1 50       8 if (my $attrs_cv = $target->can("JSONRPC_AUTOLOAD_ATTRS")) {
243 1         5 my @attrs = $attrs_cv->($target, $request);
244 1 50       1065 unless (@attrs) {
245 0         0 $self->{errstr} = "Procedure not found";
246 0         0 return $self->_error(
247             $request, $id, 0, $self->errstr, undef, $call, $target
248             );
249             }
250 1         4 $check_attrs = shift @attrs;
251             }
252             }
253             else {
254 0         0 $self->{errstr} = "Procedure not found";
255 0         0 return $self->_error(
256             $request, $id, 0, $self->errstr, undef, $call, $target
257             );
258             }
259             }
260            
261             # Named arguments defined,
262 6 100 66     44 if ($check_attrs && @$check_attrs && ref $params eq "ARRAY") {
      66        
263 3         14 my %named_params = map {
264 1         3 $_ => shift @$params
265             } @$check_attrs;
266 1         13 $params = \%named_params;
267             }
268            
269 6         7 my $rval;
270 6         8 eval {
271 6         21 $rval = $cv->($target, $request, $params);
272             };
273 6 100       4786 if ($@) {
274 3         9 $self->{errstr} = "$@";
275 3         7 $self->{errobj} = $@;
276 3 50       9 return $self->_error($request, $id, @{$@}) if ref $@ eq "ARRAY";
  0         0  
277 3         12 return $self->_error($request, $id, 0, "$@", undef, $call, $target);
278             }
279            
280 3         7 my $response;
281 3         4 eval {
282 3 50       23 $response = $self->_encode_response(200, "OK", {
283             (defined $id ? (id => $id) : ()),
284             version => "1.1",
285             result => $rval,
286             });
287             };
288 3 50       698 if ($@) {
289 0         0 $self->{errstr} = "$@";
290 0         0 $self->{errobj} = $@;
291 0         0 return $self->_error(
292             $request, $id, 0, "Failed to encode response", undef, $call, $target
293             );
294             }
295            
296 3         26 return $response;
297             }
298              
299             sub target {
300 0     0 0   my ($self, $target) = @_;
301 0           return $self->{target}->{$target};
302             }
303              
304             1;
305              
306             =head1 NAME
307              
308             JSON::RPC::Simple::Dispatcher - Decodes JSON-RPC calls and dispatches them
309              
310             =head1 DESCRIPTION
311              
312             Instances of this class decodes JSON-RPC calls over HTTP and dispatches them to
313             modules/objects registered for a given path and then encodes the result as in a
314             JSON-RPC format.
315              
316             =head1 INTERFACE
317              
318             =head2 CLASS METHODS
319              
320             =over 4
321              
322             =item new ( %opts )
323              
324             Creates a new dispatcher instance. Can take the the following optional named
325             arguments:
326              
327             =over 4
328              
329             =item json
330              
331             The encoder/decoder object to use. Defaults to L with utf8 on.
332              
333             =item charset
334              
335             The charset to send in the content-type when creating the response. Defaults
336             to C.
337              
338             =item error_handler
339              
340             A reference to a subroutine which is invoked when an error occurs. May
341             optionally return an object which will be sent as the 'error' member of the
342             result. When called it is passed the request object, the error code, error
343             message, the call ID and target object if any.
344              
345             =back
346              
347             =back
348              
349             =head2 CLASS VARIABLES
350              
351             =over 4
352              
353             =item $HTTP_ERROR_CODE
354              
355             This is the HTTP result code. It's reset to 500 (Internal Server Error) each
356             time handle is called. You may change this in your error handling routine.
357              
358             =back
359              
360             =head2 INSTANCE METHODS
361              
362             =over 4
363              
364             =item json
365              
366             =item json ( $json )
367            
368             Gets/sets the json object to use for encoding/decoding
369              
370             =item charset
371              
372             =item charset ( $charset )
373              
374             Gets/sets the charset to use when creating the HTTP::Response object.
375              
376             =item error_handler ( \&handler )
377              
378             Gets/sets the error handler to call when an error occurs.
379              
380             =item dispatch_to ( \%targets )
381              
382             Sets the dispatch table. The dispatch-table is a path to instance mapping where
383             the key is a path and the value the instance of class for which to call the
384             method on. For example
385              
386             $o->dispatch_to({
387             "/API" => "MyApp::API",
388             "/Other/API" => MyApp::OtherAPI->new(),
389             });
390              
391             =item handle ( $path, $request )
392              
393             This method decodes the $request which should be a HTTP::Request look-a-like
394             object and finds the appropriate target in the dispatch table for $path.
395              
396             The $request object MUST provide the following methods:
397              
398             =over 4
399              
400             =item method
401              
402             The HTTP method of the request such as GET, POST, HEAD, PUT in captial letters.
403              
404             =item content_type
405              
406             The Content-Type header from the request.
407              
408             =item content_length
409              
410             The Content-Length header from the request.
411              
412             =item content
413              
414             The content of the request as we only handle POST.
415              
416             =back
417              
418             The content is stripped from any unicode BOM before being passed to the JSON
419             decoder.
420              
421             =back
422              
423             =cut