File Coverage

blib/lib/Flea.pm
Criterion Covered Total %
statement 16 16 100.0
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 22 100.0


line stmt bran cond sub pod time code
1             package Flea;
2             BEGIN {
3 1     1   5960 $Flea::VERSION = '0.04';
4             }
5              
6 1     1   9 use strict;
  1         1  
  1         29  
7 1     1   5 use warnings;
  1         2  
  1         29  
8              
9 1     1   6 use Carp qw(croak);
  1         1  
  1         69  
10 1     1   928 use Exception::Class ('Flea::Pass' => { alias => 'pass' });
  1         30882  
  1         9  
11 1     1   1283 use Exporter::Declare '-magic';
  1         79949  
  1         7  
12             use JSON;
13             use HTTP::Exception;
14             use Try::Tiny;
15             use Plack::Request;
16             use URI;
17             use List::Util qw(first);
18              
19             default_exports qw(handle http route);
20             our $_add = sub { croak 'Trying to add handler outside bite' };
21              
22             sub route {
23             my ($methods, $regex, $code) = @_;
24             $_add->([map {lc} @$methods], $regex, $code);
25             }
26              
27             default_export get Flea::Parser::Route { route(['get'], @_) }
28             default_export put Flea::Parser::Route { route(['put'], @_) }
29             default_export del Flea::Parser::Route { route(['del'], @_) }
30             default_export any Flea::Parser::Route { route(['any'], @_) }
31             default_export post Flea::Parser::Route { route(['post'], @_) }
32             default_export method Flea::Parser::Method {
33             my $code = pop;
34             my $re = pop;
35             my $methods = [@_];
36             route($methods, $re, $code);
37             }
38              
39             default_export uri {
40             my ($req, $path) = @_;
41             my $base = $req->base->as_string;
42             $base =~ s|/$||;
43             $path =~ s|^/||;
44             URI->new("$base/$path")->canonical;
45             }
46              
47             default_export json {
48             return [
49             200,
50             ['Content-Type' => 'application/json; charset=UTF-8'],
51             [ JSON::encode_json(shift) ]
52             ];
53             }
54              
55             default_export html {
56             return [
57             200,
58             ['Content-Type' => 'text/html; charset=UTF-8'],
59             [ shift ]
60             ];
61             }
62              
63             default_export text {
64             return [
65             200,
66             ['Content-Type' => 'text/plain; charset=UTF-8'],
67             [ shift ]
68             ];
69             }
70              
71             sub http {
72             HTTP::Exception->throw(@_);
73             }
74              
75             sub handle {
76             my ($fh, $type) = @_;
77             return [
78             200,
79             ['Content-Type' => $type || 'text/html; charset=UTF-8'],
80             $fh
81             ];
82             }
83              
84             default_export file {
85             open my $fh, '<', shift;
86             handle($fh, @_);
87             }
88              
89             default_export request { Plack::Request->new(shift) }
90             default_export response { shift->new_response(200) }
91              
92             sub _rethrow {
93             my $e = shift;
94             $e->rethrow if ref $e && $e->can('rethrow');
95             die $e || 'unknown error';
96             }
97              
98             sub _find_and_run {
99             my ($handlers, $env) = @_;
100             my $method = lc $env->{REQUEST_METHOD};
101             my $found = 0;
102             for my $h (@$handlers) {
103             my @matches = $env->{PATH_INFO} =~ $h->{pattern};
104             if (@matches) {
105             $found = 1;
106             next unless first { $_ eq $method || $_ eq 'any' }
107             @{ $h->{methods} };
108              
109             my $result = try {
110             $h->{handler}->($env, @matches);
111             }
112             catch {
113             my $e = $_;
114             _rethrow($e) unless Flea::Pass->caught;
115             undef;
116             };
117             next unless $result;
118             return try { $result->finalize } || $result;
119             }
120             }
121             http ($found ? 405 : 404);
122             }
123              
124             default_export bite codeblock {
125             my $block = shift;
126             my @handlers;
127             local $_add = sub {
128             my ($m, $r, $c) = @_;
129             push(@handlers, { methods => $m, pattern => $r, handler => $c });
130             };
131             $block->();
132              
133             return sub { _find_and_run(\@handlers, shift) };
134             }
135              
136             1;
137              
138             =head1 NAME
139              
140             Flea - Minimalistic sugar for your Plack
141              
142             =head1 VERSION
143              
144             version 0.04
145              
146             =head1 SYNOPSIS
147              
148             # app.psgi, perhaps?
149             use Flea;
150              
151             my $app = bite {
152             get '^/$' {
153             file 'index.html';
154             }
155             get '^/api$' {
156             json { foo => 'bar' };
157             }
158             post '^/resource/(\d+)$' {
159             my $request = request(shift);
160             my $id = shift;
161             http 400 unless valid_id($id);
162             my $response = response($request)
163             $response;
164             }
165             };
166              
167             =head1 DESCRIPTION
168              
169             L/L is where it's at. L's routing syntax is really cool,
170             but it does a lot of things I don't usually want. What I really want is
171             Dancer-like sugar as an extremely thin layer over my teeth^H^H^H^H^H PSGI
172             apps.
173              
174             =head1 What's with the name?
175              
176             With all the bad tooth decay jokes, why not call it Gingivitis or something?
177             That's too much typing. And it sounds gross. Also, fleas are small and they
178             bite you when you're not paying attention. You have been warned.
179              
180             =head1 EXPORTS
181              
182             Flea is a L. Everything from there should work.
183              
184             =head2 bite
185              
186             Takes a block as an argument and returns a PSGI app. Inside the block is
187             where you define your route handlers. If you try defining them outside of a
188             route block, Flea will bite you. Note that the routing is done via path_info,
189             so your app will be mountable via L.
190              
191             =head2 get, post, put, del, any
192              
193             C will match any request method, and the others will only match the
194             corresponding method. If you need to match some other method or combination
195             of methods, see L. Aren't you glad you can rename these? (see
196             L).
197              
198             Next come a regex to match path_info against. You should surround the regex
199             with single quotes. B: are you listening? B. This
200             isn't a real perl string, it's parsed with Devel::Declare magic (you'll end up
201             with a compiled regex). If you try to use C or something cute like that,
202             you'll get B. If you need to do something fancy, use L instead
203             of these sugary things.
204              
205             Last of all comes a block. This receives the PSGI env as its first argument
206             and any matches from the regex as extra arguments. It can return either a raw
207             PSGI response or something with a finalize() method that returns a PSGI
208             response (like Plack::Response).
209              
210             =head2 method
211              
212             Just like get/post/etc, except you can tack on method names (separated by
213             spaces) to say which methods will match.
214              
215             method options '^/regex$' {
216             }
217              
218             method options head '^/regex$' {
219             }
220              
221             =head2 route($methods, $regex, $sub)
222              
223             This is an honest to goodness real perl subroutine, unlike the magic bits
224             above. You call it like:
225            
226             route ['get', 'head'], qr{^a/real/regex/please$}, sub {
227             ...
228             };
229              
230             Yes, $methods has to be an arrayref. No, $regex doesn't have to be compiled,
231             you can pass it a string if you want. But then, why are you using route?
232             Yes, you need the semicolon at the end.
233              
234             =head2 request($env)
235              
236             Short for Plack::Request->new($env)
237              
238             =head2 response($request)
239              
240             Short for $request->new_response(200).
241              
242             =head2 uri($request, $path)
243              
244             Returns a canonical L representing the path you passed with
245             $request->base welded onto the front. Does the Right Thing if $request->base
246             or $path have leading/trailing slashes. Handy for links which are internal to
247             your app, because it will still behave if you mount your app somewhere other
248             than C.
249              
250             =head2 json($str)
251              
252             Returns a full C<200 OK>, C
253             response. Pass it something that JSON::encode_json can turn into a string.
254              
255             =head2 text($str)
256              
257             text/plain; charset=UTF-8.
258              
259             =head2 html($str)
260              
261             text/html; charset=UTF-8. Seeing a pattern?
262              
263             =head2 file($filename, $mime_type?)
264              
265             Dump the contents of the file you named. If you don't give a mime type,
266             text/html is assumed.
267              
268             =head2 handle($fh, $mime_type?)
269              
270             Much like file, except you pass an open filehandle instead of a filename.
271              
272             =head2 http($code, @args)
273              
274             Shortcut for HTTP::Exception->throw. Accepts the same arguments.
275              
276             =head2 pass
277              
278             Throws a Flea::Pass exception, which causes Flea to pretend that your
279             handler didn't match and keep trying other handlers. By the way, the default
280             action when no handler is found (or they all passed) is to throw a 404
281             exception.
282              
283             =head1 MATURITY
284              
285             This module is extremely immature as of this writing. Not only does the
286             author have the mind of a child, he has never before tinkered with
287             Devel::Declare magic, although L sure does help. The
288             author hasn't thought very hard about the interface, either, so that could
289             change. When Flea breaks or doesn't do what you want, fork it on L
290             and/or send the author a patch or something. Or go use a real web framework
291             for grownups, like L.
292              
293             =head1 GITHUB
294              
295             Oh yeah, Flea is hosted on Github at L.
296              
297             =head1 IRC
298              
299             You can try hopping into #flea on irc.perl.org. The author might even be
300             there. He might even be paying attention to his irc client!
301              
302             =head1 SEE ALSO
303              
304             L, L, L, L