File Coverage

blib/lib/JSON/RPC/Common/Marshal/HTTP.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             package JSON::RPC::Common::Marshal::HTTP;
4             $JSON::RPC::Common::Marshal::HTTP::VERSION = '0.11';
5 1     1   213095 use Moose;
  0            
  0            
6             # ABSTRACT: Convert L<HTTP::Request> and L<HTTP::Response> to/from L<JSON::RPC::Common> calls and returns.
7              
8             use Carp qw(croak);
9              
10             use Try::Tiny;
11             use URI::QueryParam;
12             use MIME::Base64 ();
13             use HTTP::Response;
14              
15             use namespace::clean -except => [qw(meta)];
16              
17             extends qw(JSON::RPC::Common::Marshal::Text);
18              
19             sub _build_json {
20             JSON->new->utf8(1);
21             }
22              
23             has prefer_get => (
24             isa => "Bool",
25             is => "rw",
26             default => 0,
27             );
28              
29             has rest_style_methods => (
30             isa => "Bool",
31             is => "rw",
32             default => 1,
33             );
34              
35             has prefer_encoded_get => (
36             isa => "Bool",
37             is => "rw",
38             default => 1,
39             );
40              
41             has expand => (
42             isa => "Bool",
43             is => "rw",
44             default => 0,
45             );
46              
47             has expander => (
48             isa => "ClassName|Object",
49             lazy_build => 1,
50             handles => [qw(expand_hash collapse_hash)],
51             );
52              
53             sub _build_expander {
54             require CGI::Expand;
55             return "CGI::Expand";
56             }
57              
58              
59             has user_agent => (
60             isa => "Str",
61             is => "rw",
62             lazy_build => 1,
63             );
64              
65             sub _build_user_agent {
66             my $self = shift;
67             require JSON::RPC::Common;
68             join(" ", ref($self), $JSON::RPC::Common::VERSION),
69             }
70              
71             has content_type => (
72             isa => "Str",
73             is => "rw",
74             predicate => "has_content_type",
75             );
76              
77             has content_types => (
78             isa => "HashRef[Str]",
79             is => "rw",
80             lazy_build => 1,
81             );
82              
83             sub _build_content_types {
84             return {
85             "1.0" => "application/json",
86             "1.1" => "application/json",
87             "2.0" => "application/json-rpc",
88             };
89             }
90              
91             has accept_content_type => (
92             isa => "Str",
93             is => "rw",
94             predicate => "has_accept_content_type",
95             );
96              
97             has accept_content_types => (
98             isa => "HashRef[Str]",
99             is => "rw",
100             lazy_build => 1,
101             );
102              
103             sub _build_accept_content_types {
104             return {
105             "1.0" => "application/json",
106             "1.1" => "application/json",
107             "2.0" => "application/json-rpc",
108             };
109             }
110              
111             sub get_content_type {
112             my ( $self, $obj ) = @_;
113              
114             if ( $self->has_content_type ) {
115             return $self->content_type;
116             } else {
117             return $self->content_types->{ $obj->version || "2.0" };
118             }
119             }
120              
121             sub get_accept_content_type {
122             my ( $self, $obj ) = @_;
123              
124             if ( $self->has_accept_content_type ) {
125             return $self->accept_content_type;
126             } else {
127             return $self->accept_content_types->{ $obj->version || "2.0" };
128             }
129             }
130              
131             sub call_to_request {
132             my ( $self, $call, %args ) = @_;
133              
134             $args{prefer_get} = $self->prefer_get unless exists $args{prefer_get};
135              
136             if ( $args{prefer_get} ) {
137             return $self->call_to_get_request($call, %args);
138             } else {
139             return $self->call_to_post_request($call, %args);
140             }
141             }
142              
143             sub call_to_post_request {
144             my ( $self, $call, @args ) = @_;
145              
146             my $uri = $self->call_reconstruct_uri_base($call, @args);
147              
148             my $encoded = $self->call_to_json($call);
149              
150             my $headers = HTTP::Headers->new(
151             User_Agent => $self->user_agent,
152             Content_Type => $self->get_content_type($call),
153             Accept => $self->get_accept_content_type($call),
154             Content_Length => length($encoded),
155             );
156              
157             return HTTP::Request->new( POST => $uri, $headers, $encoded );
158             }
159              
160             sub call_to_get_request {
161             my ( $self, $call, @args ) = @_;
162              
163             my $uri = $self->call_to_uri($call, @args);
164              
165             my $headers = HTTP::Headers->new(
166             User_Agent => $self->user_agent,
167             Accept => $self->get_accept_content_type($call),
168             );
169              
170             HTTP::Request->new( GET => $uri, $headers );
171             }
172              
173             sub call_to_uri {
174             my ( $self, $call, %args ) = @_;
175              
176             no warnings 'uninitialized';
177             my $prefer_encoded_get = exists $args{encoded}
178             ? $args{encoded}
179             : ( $call->version eq '2.0' || $self->prefer_encoded_get );
180              
181             if ( $prefer_encoded_get ) {
182             return $self->call_to_encoded_uri($call, %args);
183             } else {
184             return $self->call_to_query_uri($call, %args);
185             }
186             }
187              
188             sub call_reconstruct_uri_base {
189             my ( $self, $call, %args ) = @_;
190              
191             if ( my $base_path = $args{base_path} ) {
192             return URI->new($base_path);
193             } elsif ( my $uri = $args{uri} ) {
194             $uri = $uri->clone;
195              
196             if ( my $path_info = $args{path_info} ) {
197             my $path = $uri->path;
198             $path =~ s/\Q$path_info\E$//;
199             $uri->path($path);
200             }
201              
202             return $uri;
203             } else {
204             URI->new('/');
205             }
206             }
207              
208             sub call_to_encoded_uri {
209             my ( $self, $call, @args ) = @_;
210              
211             my $uri = $self->call_reconstruct_uri_base($call, @args);
212              
213             my $deflated = $self->deflate_call($call);
214              
215             my ( $method, $params, $id ) = delete @{ $deflated }{qw(method params id)};
216              
217             my $encoded = $self->encode_base64( $self->encode($params) );
218              
219             $uri->query_param( params => $encoded );
220             $uri->query_param( method => $method );
221             $uri->query_param( id => $id ) if $call->has_id;
222              
223             return $uri;
224             }
225              
226             sub call_to_query_uri {
227             my ( $self, $call, %args ) = @_;
228              
229             my $uri = $self->call_reconstruct_uri_base($call, %args);
230              
231             my $deflated = $self->deflate_call( $call );
232              
233             my ( $method, $params, $id ) = delete @{ $deflated }{qw(method params id)};
234              
235             $params = $self->collapse_query_params($params);
236              
237             $uri->query_form( %$params, id => $id );
238              
239             if ( exists $args{rest_style_methods} ? $args{rest_style_methods} : $self->rest_style_methods ) {
240             my $path = $uri->path;
241             $path =~ s{/?$}{"/" . $method}e; # add method, remove double trailing slash
242             $uri->path($path);
243             } else {
244             $uri->query_param( method => $method );
245             }
246              
247             return $uri;
248             }
249              
250             sub request_to_call {
251             my ( $self, $request, @args ) = @_;
252              
253             my $req_method = lc( $request->method . "_request_to_call" );
254              
255             if ( my $code = $self->can($req_method) ) {
256             $self->$code($request, @args);
257             } else {
258             croak "Unsupported HTTP request method " . $request->method;
259             }
260             }
261              
262             sub get_request_to_call {
263             my ( $self, $request, @args ) = @_;
264              
265             $self->uri_to_call(request => $request, @args);
266             }
267              
268             sub uri_to_call {
269             my ( $self, %args ) = @_;
270              
271             my $uri = $args{uri} || ($args{request} || croak "Either 'uri' or 'request' is mandatory")->uri;
272              
273             my $params = $uri->query_form_hash;
274              
275             if ( exists $params->{params} and $self->prefer_encoded_get ) {
276             return $self->encoded_uri_to_call( $uri, %args );
277             } else {
278             return $self->query_uri_to_call( $uri, %args );
279             }
280             }
281              
282             sub decode_base64 {
283             my ( $self, $base64 ) = @_;
284             MIME::Base64::decode_base64($base64);
285             }
286              
287             sub encode_base64 {
288             my ( $self, $base64 ) = @_;
289             MIME::Base64::encode_base64($base64);
290             }
291              
292             # the sane way, 1.1-alt
293             sub encoded_uri_to_call {
294             my ( $self, $uri, @args ) = @_;
295              
296             my $params = $uri->query_form_hash;
297              
298             # the 'params' URI param is encoded as JSON, inflate it
299             my %rpc = %$params;
300              
301             $rpc{version} ||= "2.0";
302              
303             for my $params ( $rpc{params} ) {
304             # try as unencoded JSON first
305             if ( my $data = try { $self->decode($params) } ) {
306             $params = $data;
307             } else {
308             my $json = $self->decode_base64($params) || croak "params are not Base64 encoded";
309             $params = $self->decode($json);
310             }
311             }
312              
313             $self->inflate_call(\%rpc);
314             }
315              
316             # the less sane but occasionally useful way, 1.1-wd
317             sub query_uri_to_call {
318             my ( $self, $uri, %args ) = @_;
319              
320             my $params = $uri->query_form_hash;
321              
322             my %rpc = ( params => $params );
323              
324             foreach my $key (qw(version jsonrpc method id) ) {
325             if ( exists $params->{$key} ) {
326             $rpc{$key} = delete $params->{$key};
327             }
328             }
329              
330             if ( !exists($rpc{method}) and $args{rest_style_methods} || $self->rest_style_methods ) {
331             if ( my $path_info = $args{path_info} ) {
332             ( $rpc{method} = $path_info ) =~ s{^/}{};
333             } elsif ( my $base = $args{base_path} ) {
334             my ( $method ) = ( $uri->path =~ m{^\Q$base\E(.*)$} );
335             $method =~ s{^/}{};
336             $rpc{method} = $method;
337             } else {
338             my ( $method ) = ( $uri->path =~ m{/(\w+)$} );
339             $rpc{method} = $method;
340             }
341             }
342              
343             $rpc{version} ||= "1.1";
344              
345             # increases usefulness
346             $rpc{params} = $self->expand_query_params($params, %args);
347              
348             $self->inflate_call(\%rpc);
349             }
350              
351             sub expand_query_params {
352             my ( $self, $params, @args ) = @_;
353              
354             if ( $self->expand ) {
355             return $self->expand_hash($params);
356             } else {
357             return $params;
358             }
359             }
360              
361             sub collapse_query_params {
362             my ( $self, $params, $request, @args ) = @_;
363              
364             if ( $self->expand ) {
365             return $self->collapse_hash($params);
366             } else {
367             return $params;
368             }
369             }
370              
371             sub post_request_to_call {
372             my ( $self, $request ) = @_;
373             $self->json_to_call( $request->content );
374             }
375              
376             sub write_result_to_response {
377             my ( $self, $result, $response, @args ) = @_;
378              
379             my %args = $self->result_to_response_params($result);
380              
381             foreach my $key ( keys %args ) {
382             if ( $response->can($key) ) {
383             $response->$key(delete $args{$key});
384             }
385             }
386              
387             if (my @keys = keys %args) {
388             croak "Unhandled response params: " . join ' ', @keys;
389             }
390              
391             return 1;
392             }
393              
394             sub response_to_result {
395             my ( $self, $response ) = @_;
396              
397             if ( $response->is_success ) {
398             $self->response_to_result_success($response);
399             } else {
400             $self->response_to_result_error($response);
401             }
402             }
403              
404             sub response_to_result_success {
405             my ( $self, $response ) = @_;
406              
407             $self->json_to_return( $response->content );
408             }
409              
410             sub response_to_result_error {
411             my ( $self, $response ) = @_;
412              
413             my $res = $self->json_to_return( $response->content );
414              
415             unless ( $res->has_error ) {
416             $res->set_error(
417             message => $response->message,
418             code => $response->code, # FIXME dictionary
419             data => {
420             response => $response,
421             }
422             );
423             }
424              
425             return $res;
426             }
427              
428             sub result_to_response {
429             my ( $self, $result ) = @_;
430              
431             $self->create_http_response( $self->result_to_response_headers($result) );
432             }
433              
434             sub create_http_response {
435             my ( $self, %args ) = @_;
436              
437             my ( $body, $status ) = delete @args{qw(body status)};
438              
439             HTTP::Response->new(
440             $status,
441             undef,
442             HTTP::Headers->new(%args),
443             $body,
444             );
445             }
446              
447             sub result_to_response_headers {
448             my ( $self, $result ) = @_;
449              
450             my $body = $self->encode($result->deflate);
451              
452             return (
453             status => ( $result->has_error ? $result->error->http_status : 200 ),
454             Content_Type => $self->get_content_type($result),
455             Content_Length => length($body), # http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html#ResponseHeaders
456             body => $body,
457             );
458             }
459              
460             sub result_to_response_params {
461             my ( $self, $result ) = @_;
462              
463             my %headers = $self->result_to_response_headers($result);
464             $headers{content_type} = delete $headers{Content_Type};
465             $headers{content_length} = delete $headers{Content_Length};
466              
467             return %headers;
468             }
469              
470             __PACKAGE__->meta->make_immutable();
471              
472             __PACKAGE__
473              
474             __END__
475              
476             =pod
477              
478             =head1 NAME
479              
480             JSON::RPC::Common::Marshal::HTTP - Convert L<HTTP::Request> and L<HTTP::Response> to/from L<JSON::RPC::Common> calls and returns.
481              
482             =head1 VERSION
483              
484             version 0.11
485              
486             =head1 SYNOPSIS
487              
488             use JSON::RPC::Common::Marshal::HTTP;
489              
490             my $m = JSON::RPC::Common::Marshal::HTTP->new;
491              
492             my $call = $m->request_to_call($http_request);
493              
494             my $res = $call->call($object);
495              
496             my $http_response = $m->result_to_response($res);
497              
498             =head1 DESCRIPTION
499              
500             This object provides marshalling routines to convert calls and returns to and
501             from L<HTTP::Request> and L<HTTP::Response> objects.
502              
503             =head1 ATTRIBUTES
504              
505             =over 4
506              
507             =item prefer_get
508              
509             When encoding a call into a request, prefer GET.
510              
511             Not reccomended.
512              
513             =item rest_style_methods
514              
515             When encoding a GET request, use REST style URI formatting (the method is part
516             of the path, not a parameter).
517              
518             =item prefer_encoded_get
519              
520             When set and a C<params> param exists, decode it as Base 64 encoded JSON and
521             use that as the parameters instead of the query parameters.
522              
523             See L<http://json-rpc.googlegroups.com/web/json-rpc-over-http.html>.
524              
525             =item user_agent
526              
527             Defaults to the marshal object's class name and the L<JSON::RPC::Common>
528             version number.
529              
530             =item content_type
531              
532             =item accept_content_type
533              
534             =item content_types
535              
536             =item accept_content_types
537              
538             When explicitly set these are the values of the C<Content-Type> and C<Accept>
539             headers to set.
540              
541             Otherwise they will default to C<application/json> with calls/returns version
542             1.0 and 1.1, and C<application/json-rpc> with 2.0 objects.
543              
544             =item expand
545              
546             Whether or not to use an expander on C<GET> style calls.
547              
548             =item expander
549              
550             An instance of L<CGI::Expand> or a look alike to use for C<GET> parameter
551             expansion.
552              
553             =back
554              
555             =head1 METHODS
556              
557             =over 4
558              
559             =item request_to_call $http_request
560              
561             =item post_request_to_call $http_request
562              
563             =item get_request_to_call $http_request
564              
565             Convert an L<HTTP::Request> to a L<JSON::RPC::Common::Procedure::Call>.
566             Depending on what style of request it is, C<request_to_call> will delegate to a
567             variant method.
568              
569             Get requests call C<uri_to_call>
570              
571             =item uri_to_call $uri
572              
573             =item encoded_uri_to_call $uri
574              
575             =item query_uri_to_call $uri
576              
577             Parse a call from a GET request's URI.
578              
579             =item result_to_response $return
580              
581             Convert a L<JSON::RPC::Common::Procedure::Return> to an L<HTTP::Response>.
582              
583             =item write_result_to_response $result, $response
584              
585             Write the result into an object like L<Catalyst::Response>.
586              
587             =item response_to_result $http_response
588              
589             =item response_to_result_success $http_response
590              
591             =item response_to_result_error $http_response
592              
593             Convert an L<HTTP::Response> to a L<JSON::RPC::Common::Procedure::Return>.
594              
595             A variant is chosen based on C<HTTP::Response/is_success>.
596              
597             The error handler will ensure that
598             L<JSON::RPC::Common::Procedure::Return/error> is set.
599              
600             =item call_to_request $call, %args
601              
602             =item call_to_get_request $call, %args
603              
604             =item call_to_post_request $call, %args
605              
606             =item call_to_uri $call, %args
607              
608             =item call_to_encoded_uri $call, %args
609              
610             =item call_to_query_uri $call, %args
611              
612             Convert a call to a request (or just a URI for GET requests).
613              
614             The arguments can contain a C<uri> parameter, which is the base of the request.
615              
616             With GET requests, under C<rest_style_methods> that URI's path will be
617             appended, and otherwise parameters will just be added.
618              
619             POST requests do not cloen and alter the URI.
620              
621             If no URI is provided as an argument, C</> will be used.
622              
623             The flags C<prefer_get> and C<encoded> can also be passed to
624             C<call_to_request> to alter the type of request to be generated.
625              
626             =item collapse_query_params
627              
628             =item expand_query_params
629              
630             Only used for query encoded GET requests. If C<expand> is set will cause
631             expansion of the params. Otherwise it's a noop.
632              
633             Subclass and override to process query params into RPC params as necessary.
634              
635             Note that this is B<NOT> in any of the JSON-RPC specs.
636              
637             =back
638              
639             =head1 AUTHOR
640              
641             Yuval Kogman <nothingmuch@woobling.org>
642              
643             =head1 COPYRIGHT AND LICENSE
644              
645             This software is copyright (c) 2014 by Yuval Kogman and others.
646              
647             This is free software; you can redistribute it and/or modify it under
648             the same terms as the Perl 5 programming language system itself.
649              
650             =cut