File Coverage

blib/lib/Catalyst/View/Base/JSON.pm
Criterion Covered Total %
statement 48 64 75.0
branch 9 26 34.6
condition 3 12 25.0
subroutine 11 18 61.1
pod 6 12 50.0
total 77 132 58.3


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