File Coverage

blib/lib/McBain/WithPSGI.pm
Criterion Covered Total %
statement 40 40 100.0
branch 6 8 75.0
condition n/a
subroutine 11 11 100.0
pod 4 4 100.0
total 61 63 96.8


line stmt bran cond sub pod time code
1             package McBain::WithPSGI;
2              
3             # ABSTRACT: Load a McBain API as a RESTful PSGI web service
4              
5 2     2   64293 use warnings;
  2         3  
  2         62  
6 2     2   8 use strict;
  2         3  
  2         52  
7              
8 2     2   8 use Carp;
  2         5  
  2         125  
9 2     2   828 use JSON::MaybeXS qw/JSON/;
  2         7951  
  2         106  
10 2     2   1069 use Plack::Request;
  2         92369  
  2         62  
11 2     2   868 use Plack::Component;
  2         5855  
  2         168  
12              
13             our $VERSION = "2.001001";
14             $VERSION = eval $VERSION;
15              
16             my $json = JSON->new->utf8->allow_blessed->convert_blessed;
17              
18             =head1 NAME
19            
20             McBain::WithPSGI - Load a McBain API as a RESTful PSGI web service
21              
22             =head1 SYNOPSIS
23              
24             # write your API as you normally would, and create
25             # a simple psgi file:
26              
27             #!/usr/bin/perl -w
28              
29             use warnings;
30             use strict;
31             use MyAPI -withPSGI;
32              
33             my $app = MyAPI->to_app;
34              
35             =head1 DESCRIPTION
36              
37             C turns your L API into a RESTful L web service based on
38             L, thus making C a web application framework.
39              
40             The created web service will be a JSON-in JSON-out service. Requests to your application
41             are expected to have a C of C. The JSON body
42             of a request will be the payload. The results of the API will be formatted into JSON as
43             well.
44              
45             Note that if an API method does not return a hash-ref, this runner module will automatically
46             turn it into a hash-ref to ensure that conversion into JSON will be possible. The created
47             hash-ref will have one key - holding the method's name, with whatever was returned from the
48             method as its value. For example, if method C in topic C returns an
49             integer (say 7), then the client will get the JSON C<{ "GET:/math/divide": 7 }>.
50              
51             =head2 SUPPORTED HTTP METHODS
52              
53             This runner support all methods natively supported by L. That is: C, C,
54             C, C and C. To add support for C requests, use L.
55              
56             The C method is special. It returns a list of all HTTP methods allowed by a specific
57             route (in the C header). The response body will be the same hash-ref returned by
58             C for C requests, JSON encoded.
59              
60             =head2 CAVEATS AND CONSIDERATIONS
61              
62             The HTTP protocol does not allow C requests to have content, so your C routes will
63             not be able to receive parameters from a request's JSON body as all other methods do.
64             If your C routes I receive parameters (for example, you might have a route that
65             returns a list of objects with support for pagination), C supports parameters
66             from the query string. They will be validated like all parameters, and they can be used in
67             non-C requests too. Note that they take precedence over body parameters.
68              
69             The downside to this is that the parameters cannot be complex structures, though if the query string
70             defines a certain key several times, its generated value will be an array reference. For example,
71             let's look at the following route:
72              
73             get '/params_from_query' => (
74             params => {
75             some_string => { required => 1 },
76             some_array => { array => 1, min_length => 2 }
77             },
78             cb => sub {
79             my ($api, $params) = @_;
80             return $params;
81             }
82             );
83              
84             This route isn't particularly interesting, as it simply returns the parameters it receives. It does,
85             however, enforce the existence of the C parameter, and expects C to be an
86             array reference of at least 2 items. A request to C will yield the
87             following result:
88              
89             { "some_string": "this_is_my_string", "some_array": ["Hello", "World"] }
90              
91             =head1 METHODS EXPORTED TO YOUR API
92              
93             None.
94              
95             =head1 METHODS REQUIRED BY MCBAIN
96              
97             =head2 init( $target )
98              
99             Makes the root topic of your API inherit L, so that it
100             officially becomes a Plack app. This will provide your API with the C
101             method.
102              
103             =cut
104              
105             sub init {
106 1     1 1 13 my ($class, $target) = @_;
107              
108 1 50       15 if ($target->is_root) {
109 2     2   11 no strict 'refs';
  2         3  
  2         574  
110 1         6 push(@{"${target}::ISA"}, 'Plack::Component');
  1         33  
111             }
112             }
113              
114             =head2 generate_env( $psgi_env )
115              
116             Receives the PSGI env hash-ref and creates McBain's standard env hash-ref
117             from it.
118              
119             =cut
120              
121             sub generate_env {
122 20     20 1 50458 my ($self, $psgi_env) = @_;
123              
124 20         96 my $req = Plack::Request->new($psgi_env);
125              
126 20 50       166 my $payload = $req->content ? $json->decode($req->content) : {};
127              
128             # also take parameters from query string, if any
129             # and let them have precedence over request content
130 20         14700 my $query = $req->query_parameters->mixed;
131 20         946 foreach (keys %$query) {
132 2         5 $payload->{$_} = $query->{$_};
133             }
134              
135             return {
136 20         57 METHOD => $req->method,
137             ROUTE => $req->path,
138             PAYLOAD => $payload
139             };
140             }
141              
142             =head2 generate_res( $env, $res )
143              
144             Converts the result of an API method to JSON, and returns a standard PSGI
145             response array-ref.
146              
147             =cut
148              
149             sub generate_res {
150 15     15 1 4022 my ($self, $env, $res) = @_;
151              
152 15         35 my @headers = ('Content-Type' => 'application/json; charset=UTF-8');
153              
154 15 100       41 if ($env->{METHOD} eq 'OPTIONS') {
155 1         4 push(@headers, 'Allow' => join(',', keys %$res));
156             }
157              
158 15 100       70 $res = { $env->{METHOD}.':'.$env->{ROUTE} => $res }
159             unless ref $res eq 'HASH';
160              
161 15         160 return [200, \@headers, [$json->encode($res)]];
162             }
163              
164             =head2 handle_exception( $err )
165              
166             Formats exceptions into JSON and returns a standard PSGI array-ref.
167              
168             =cut
169              
170             sub handle_exception {
171 5     5 1 1032 my ($class, $err) = @_;
172              
173 5         73 return [delete($err->{code}), ['Content-Type' => 'application/json; charset=UTF-8'], [$json->encode($err)]];
174             }
175              
176             =head1 CONFIGURATION AND ENVIRONMENT
177              
178             No configuration files are required.
179              
180             =head1 DEPENDENCIES
181            
182             C depends on the following CPAN modules:
183            
184             =over
185              
186             =item * L
187              
188             =item * L
189            
190             =item * L
191            
192             =back
193              
194             =head1 INCOMPATIBILITIES WITH OTHER MODULES
195              
196             None reported.
197              
198             =head1 BUGS AND LIMITATIONS
199              
200             Please report any bugs or feature requests to
201             C, or through the web interface at
202             L.
203              
204             =head1 SUPPORT
205              
206             You can find documentation for this module with the perldoc command.
207              
208             perldoc McBain::WithPSGI
209              
210             You can also look for information at:
211              
212             =over 4
213            
214             =item * RT: CPAN's request tracker
215            
216             L
217            
218             =item * AnnoCPAN: Annotated CPAN documentation
219            
220             L
221            
222             =item * CPAN Ratings
223            
224             L
225            
226             =item * Search CPAN
227            
228             L
229            
230             =back
231            
232             =head1 AUTHOR
233            
234             Ido Perlmuter
235            
236             =head1 LICENSE AND COPYRIGHT
237            
238             Copyright (c) 2013-2015, Ido Perlmuter C<< ido@ido50.net >>.
239            
240             This module is free software; you can redistribute it and/or
241             modify it under the same terms as Perl itself, either version
242             5.8.1 or any later version. See L
243             and L.
244            
245             The full text of the license can be found in the
246             LICENSE file included with this module.
247            
248             =head1 DISCLAIMER OF WARRANTY
249            
250             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
251             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
252             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
253             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
254             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
255             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
256             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
257             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
258             NECESSARY SERVICING, REPAIR, OR CORRECTION.
259            
260             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
261             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
262             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
263             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
264             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
265             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
266             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
267             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
268             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
269             SUCH DAMAGES.
270            
271             =cut
272              
273             1;
274             __END__