File Coverage

blib/lib/Catalyst/View/Base/JSON.pm
Criterion Covered Total %
statement 48 64 75.0
branch 10 26 38.4
condition 4 11 36.3
subroutine 11 18 61.1
pod 6 12 50.0
total 79 131 60.3


line stmt bran cond sub pod time code
1 1     1   554 use strict;
  1         1  
  1         26  
2 1     1   3 use warnings;
  1         1  
  1         38  
3              
4             package Catalyst::View::Base::JSON;
5              
6 1     1   3 use base 'Catalyst::View';
  1         6  
  1         382  
7 1     1   9215 use HTTP::Status;
  1         2  
  1         243  
8 1     1   5 use Scalar::Util;
  1         0  
  1         967  
9              
10             our $VERSION = 0.001;
11             our $CLASS_INFO = 'Catalyst::View::Base::JSON::_ClassInfo';
12              
13             my $inject_http_status_helpers = sub {
14             my ($class, $args) = @_;
15             return unless $args->{returns_status};
16             foreach my $helper( grep { $_=~/^http/i} @HTTP::Status::EXPORT_OK) {
17             my $subname = lc $helper;
18             my $code = HTTP::Status->$helper;
19             my $codename = "http_".$code;
20             if(grep { $code == $_ } @{ $args->{returns_status}||[]}) {
21 0     0 0 0 eval "sub ${\$class}::${\$subname} { return shift->response(HTTP::Status::$helper,\@_) }";
  1     1 0 75  
22 0     0 0 0 eval "sub ${\$class}::${\$codename} { return shift->response(HTTP::Status::$helper,\@_) }";
  0     0 0 0  
23             }
24             }
25             };
26              
27             my $find_fields = sub {
28             my $class = shift;
29             my @fields = ();
30             for ($class->meta->get_all_attributes) {
31             next unless $_->has_init_arg;
32             push @fields, $_->init_arg;
33             }
34             return @fields;
35             };
36              
37             sub _build_class_info {
38 1     1   3 my ($class, $args) = @_;
39 1         5 Catalyst::Utils::ensure_class_loaded($CLASS_INFO);
40 1         12 return $CLASS_INFO->new($args);
41             }
42              
43             sub COMPONENT {
44 1     1 1 217854 my ($class, $app, $args) = @_;
45 1         7 $args = $class->merge_config_hashes($class->config, $args);
46 1         21081 $args->{_instance_class} = $class;
47 1         2 $args->{_original_args} = $args;
48 1         4 $args->{_fields} = [$class->$find_fields];
49 1         4 $class->$inject_http_status_helpers($args);
50              
51 1         5 return $class->_build_class_info($args);
52             }
53              
54 1     1 1 23 sub ctx { return $_[0]->{__ctx} }
55 0     0 1 0 sub process { return shift->response(200, @_) }
56 0     0 0 0 sub detach { shift->ctx->detach(@_) }
57              
58             my $class_info = sub { return $_[0]->{__class_info} };
59              
60             sub response {
61 1     1 1 2 my ($self, @proto) = @_;
62            
63 1         2 my $status = 200;
64 1 50 33     10 if( (ref \$proto[0] eq 'SCALAR') and
65             Scalar::Util::looks_like_number($proto[0])
66             ){
67 1         2 $status = shift @proto;
68             }
69              
70 1         2 my $possible_override_data = '';
71 1 50 50     9 if(
      33        
72             ((ref($proto[-1])||'') eq 'HASH') ||
73             Scalar::Util::blessed($proto[-1])
74             ) {
75 0         0 $possible_override_data = pop(@proto);
76             }
77            
78 1         7 my @headers = ();
79 1 50       3 if(@proto) {
80 0         0 @headers = @proto;
81             }
82              
83 1         6 for($self->ctx->response) {
84 1 50       9 $_->headers->push_header(@headers) if @headers;
85 1 50       6 $_->status($status) unless $_->status != 200; # Catalyst default is 200...
86 1 50       159 $_->content_type($self->$class_info->content_type)
87             unless $_->content_type;
88              
89 1 50       140 $self->amend_headers($_->headers)
90             if $self->can('amend_headers');
91              
92 1 50       8 unless($_->has_body) {
93 1         32 my $json = $self->render($possible_override_data);
94 1 50       3 if(my $param = $self->$class_info->callback_param) {
95 0         0 my $cb = $_->query_parameter($self->$class_info->callback_param);
96 0 0       0 $cb =~ /^[a-zA-Z0-9\.\_\[\]]+$/ || die "Invalid callback parameter $cb";
97 0         0 $json = "$cb($json)";
98             }
99 1         25 $_->body($json);
100             }
101             }
102             }
103              
104             sub render {
105 1     1 1 2 my ($self, $possible_override_data) = @_;
106 1 50       3 my $to_json_encode = $possible_override_data ? $possible_override_data : $self;
107             my $json = eval {
108             $self->$class_info->json->encode($to_json_encode);
109 1   33     2 } || do {
110             $self->$class_info->HANDLE_ENCODE_ERROR($self, $to_json_encode, $@);
111             };
112 1         55 return $json;
113             }
114              
115             sub uri {
116 0     0 1   my ($self, $action_proto, @args) = @_;
117              
118             # Is an action object
119 0 0         return $self->ctx->uri_for($action_proto, @args)
120             if Scalar::Util::blessed($action_proto);
121              
122             # Is an absolute or relative (to the current controller) action private name.
123 0 0         my $action = $action_proto=~m/^\// ?
124             $self->ctx->dispatcher->get_action_by_path($action_proto) :
125             $self->ctx->controller->action_for($action_proto);
126            
127 0           return $self->ctx->uri_for($action, @args);
128             }
129              
130 0     0 0   sub TO_JSON { die "View ${\$_[0]->catalyst_component_name} must define a 'TO_JSON' method!" }
  0            
131              
132             1;
133              
134             =head1 NAME
135              
136              
137             Catalyst::View::Base::JSON - a 'base' JSON View
138              
139             =head1 SYNOPSIS
140              
141             package MyApp::View::Person;
142              
143             use Moo;
144             use Types::Standard;
145             use MyApp::Types qw/Version/;
146              
147             extends 'Catalyst::View::Base::JSON';
148              
149             has name => (
150             is=>'ro',
151             isa=>Str,
152             required=>1);
153              
154             has age => (
155             is=>'ro',
156             isa=>Int,
157             required=>1);
158              
159             has api_version => (
160             is=>'ro',
161             isa=>Version,
162             required=>1);
163              
164             sub amend_headers {
165             my ($self, $headers) = @_;
166             $headers->push_header(Accept => 'application/json');
167             }
168              
169             sub TO_JSON {
170             my $self = shift;
171             return +{
172             name => $self->name,
173             age => $self->age,
174             api => $self->api_version,
175             };
176             }
177              
178             package MyApp::Controller::Root;
179             use base 'Catalyst::Controller';
180              
181             sub example :Local Args(0) {
182             my ($self, $c) = @_;
183             $c->stash(age=>32);
184             $c->view('Person', name=>'John')->http_ok;
185             }
186              
187             package MyApp;
188            
189             use Catalyst;
190              
191             MyApp->config(
192             'Controller::Root' => { namespace => '' },
193             'View::Person' => {
194             returns_status => [200, 404],
195             api_version => '1.1',
196             },
197             );
198              
199             MyApp->setup;
200              
201              
202             =head1 DESCRIPTION
203              
204             This is a Catalyst view that lets you create one view per reponse type of JSON
205             you are generating. Because you are creating one view per reponse type that means
206             you can define an interface for that view which is strongly typed. Also, since
207             the view is per request, it has access to the context, as well as some helpers
208             for creating URLs. You may find that this helps make your controllers more
209             simple and promote reuse of view code.
210              
211             I consider this work partly a thought experiment. Documentation and test coverage
212             are currently light and I might change parts of the way exceptions are handled. If
213             you are producing JSON with L<Catalyst> and new to the framework you might want to
214             consider 'tried and true' approaches such as L<Catalyst::View:::JSON> or
215             L<Catalyst::Action::REST>. My intention here is to get people to start thinking
216             about views with stronger interfaces.
217              
218             =head1 METHODS
219              
220             This view defines the following methods
221              
222             =head2 response
223              
224             $view->response($status);
225             $view->response($status, @headers);
226             $view->response(@headers);
227              
228              
229             Used to setup a response. Calling this method will setup an http status, finalize
230             headers and set a body response for the JSON. Content type will be set based on
231             your 'content_type' configuration value (or 'application/json' by default).
232              
233             =head2 Method '->response' Helpers
234              
235             We map status codes from L<HTTP::Status> into methods to make sending common
236             request types more simple and more descriptive. The following are the same:
237              
238             $c->view->response(200, @args);
239             $c->view->http_ok(@args);
240              
241             do { $c->view->response(200, @args); $c->detach };
242             $c->view->http_ok(@args)->detach;
243              
244             See L<HTTP::Status> for a full list of all the status code helpers.
245              
246             =head2 ctx
247              
248             Returns the current context associated with the request creating this view.
249              
250             =head2 uri ($action|$action_name|$relative_action_name)
251              
252             Helper used to create links. Example:
253              
254             sub TO_JSON {
255             my $self = shift;
256             return +{
257             name => $self->name,
258             age => $self->age,
259             friends => $self->uri('friends', $self->id),
260             };
261             }
262              
263             The arguments are basically the same as $c->uri_for except that the first argument
264             may be a full or relative action path.
265              
266             =head2 render
267              
268             Returns a string which is the JSON represenation of the current View. Usually you
269             won't need to call this directly.
270              
271             =head2 process
272              
273             used as a target for $c->forward. This is mostly here for compatibility with some
274             existing methodology. For example allows using this View with the RenderView action
275             class (or L<Catalyst::Action::RenderView>).
276              
277             =head1 ATTRIBUTES
278              
279             See L<Catalyst::View::Base::JSON::_ClassInfo> for application level configuration.
280             You may also defined custom attributes in your base class and assign values via
281             configuration.
282              
283             =head1 UTF-8 NOTES
284              
285             Generally a view should not do any encoding since the core L<Catalyst>
286             framework handles all this for you. However, historically the popular
287             Catalyst JSON views and related ecosystem (such as L<Catalyst::Action::REST>)
288             have done UTF8 encoding and as a result for compatibility core Catalyst code
289             will assume a response content type of 'application/json' is already UTF8
290             encoded. So even though this is a new module, we will continue to maintain this
291             historical situation for compatibility reasons. As a result the UTF8 encoding
292             flags will be enabled and expect the contents of $c->res->body to be encoded
293             as expected. If you set your own JSON class for encoding, or set your own
294             initialization arguments, please keep in mind this expectation.
295              
296             =head1 SEE ALSO
297              
298             L<Catalyst>, L<Catalyst::View>, L<Catalyst::View::JSON>,
299             L<JSON::MaybeXS>
300              
301             =head1 AUTHOR
302            
303             John Napiorkowski L<email:jjnapiork@cpan.org>
304            
305             =head1 COPYRIGHT & LICENSE
306            
307             Copyright 2016, John Napiorkowski L<email:jjnapiork@cpan.org>
308            
309             This library is free software; you can redistribute it and/or modify it under
310             the same terms as Perl itself.
311              
312             =cut