File Coverage

blib/lib/HTTP/Server/Simple/Dispatched.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             package HTTP::Server::Simple::Dispatched;
2              
3             =head1 NAME
4              
5             HTTP::Server::Simple::Dispatched - Django-like regex dispatching with request and response objects - no CGI.pm cruft!
6              
7             =head1 VERSION
8              
9             Version 0.06
10              
11             =cut
12              
13 1     1   26698 use Moose;
  0            
  0            
14             use Moose::Util::TypeConstraints;
15             our $VERSION = '0.06';
16              
17             extends qw(
18             HTTP::Server::Simple
19             Exporter
20             );
21              
22             use URI;
23             use URI::Escape qw(uri_unescape);
24             use MIME::Types;
25             use File::Spec::Functions qw(rel2abs);
26              
27             use HTTP::Server::Simple::Dispatched::Request;
28             use HTTP::Response;
29              
30             use Data::Dumper;
31             use Devel::StackTrace;
32             use Carp;
33              
34             =head1 SYNOPSIS
35              
36             Quick and dirty regex-based dispatching inspired by Django, with standard response and request objects to ease some of the pain of HTTP::Server::Simple. Lower level than CGI.pm, which allows you to lose some of the cruft of that hoary old monster.
37              
38             use HTTP::Server::Simple::Dispatched qw(static);
39              
40             my $server = HTTP::Server::Simple::Dispatched->new(
41             hostname => 'myawesomeserver.org',
42             port => 8081,
43             debug => 1,
44             dispatch => [
45             qr{^/hello/} => sub {
46             my ($response) = @_;
47             $response->content_type('text/plain');
48             $response->content("Hello, world!");
49             return 1;
50             },
51             qr{^/say/(\w+)/} => sub {
52             my ($response) = @_;
53             $response->content_type('text/plain');
54             $response->content("You asked me to say $1.");
55             return 1;
56             },
57             qr{^/counter/} => sub {
58             my ($response, $request, $context) = @_;
59             my $num = ++$context->{counter};
60             $response->content_type('text/plain');
61             $response->content("Called $num times.");
62             return 1;
63             },
64             qr{^/static/(.*\.(?:png|gif|jpg))} => static("t/"),
65             qr{^/error/} => sub {
66             die "This will cause a 500!";
67             },
68             ],
69             );
70              
71             $server->run();
72              
73             =cut
74              
75             sub _valid_dispatch_map {
76             my $aref = $_[0];
77             return 0 if (@$aref % 2);
78              
79             for(my $i = 0; $i < @$aref; ) {
80             my $pattern = $aref->[$i++];
81             my $handler = $aref->[$i++];
82             if (ref $pattern ne 'Regexp' || ref $handler ne 'CODE') {
83             return 0;
84             }
85             }
86              
87             return 1;
88             }
89             subtype DispatchMap => as ArrayRef => where {_valid_dispatch_map($_)};
90              
91             =head1 EXPORTED VARIABLES
92              
93             =head2 $mime
94              
95             The registry of mime types, this is a MIME::Types and is referenced
96             during the serving of static files. Not exported by default.
97              
98             =cut
99              
100             our $mime = MIME::Types->new();
101              
102             =head1 EXPORTED FUNCTIONS
103              
104             =head2 static
105              
106             Use this in dispatch specifiers to install a static-file handler. It takes one argument, a "root" directory. Your regex must capture the path from that root as $1 - e.g. "qr{^/some_path/(.*\.(?:png))} => static("foo/") to serve only .png files from foo/ as "/some_path/some.png". See the the 'static' example in the synopsis. Not exported by default.
107              
108             =cut
109              
110             sub static {
111             my $root = rel2abs($_[0]);
112             my $child_match = qr{^$root/.*};
113             my $default_type = $mime->type('application/octet-stream');
114              
115             return sub {
116             my ($response, $request) = @_;
117             eval {
118             my $path = rel2abs(uri_unescape($1), $root);
119             $path =~ $child_match or die {code => 404};
120              
121             my $fh = IO::File->new($path, '<') or
122             die {code => (-e $path ? 403 : 404)};
123              
124             my $type = $mime->mimeTypeOf($path) || $default_type;
125             $fh->binmode() if $type->isBinary;
126              
127             my $content;
128             {local $/; $content = <$fh>};
129             $fh->close();
130              
131             $content ||= q();
132             $response->content($content);
133             $response->content_type($content ? $type : 'text/plain');
134             };
135             return 1 unless $@;
136              
137             if (ref $@ eq 'HASH' and exists $@->{code}) {
138             $response->code($@->{code});
139             return 1;
140             }
141              
142             # Other errors mean 500 with debug info
143             die $@;
144             };
145             }
146              
147             our @EXPORT_OK = qw(static $mime);
148              
149             =head1 ATTRIBUTES
150              
151             These are Moose attributes: see its documentation for details, or treat them like regular perl read/write object accessors with convenient keyword arguments in the constructor.
152              
153             =head2 dispatch
154              
155             An arrayref of regex object => coderef pairs. This bit is why you're using this module - you can map urls to handlers and capture pieces of the url. Any captures in the regex are bound to $1..$n just like in a normal regex. See the 'say' example in the synopsis. Note: these are matched in the order you specify them, so beware permissive regexes!
156              
157             =over 2
158              
159             =item
160              
161             Handlers receive three arguments: An HTTP::Response, an HTTP::Server::Simple::Dispatched::Request, and the context object. The response object defaults to a 200 OK response with text/html as the content type.
162              
163             =item
164              
165             Your handler should return a true value if it handles the request - return 0 otherwise (that entry didn't exist in the database, etc.) and a standard 404 will be generated unless another handler picks it up.
166              
167             =item
168              
169             Content-Length will be set for you if you do not set it yourself. This is I<probably> what you want. If you do not, manually set Content-Length to whatever you think it should be.
170              
171             =item
172              
173             Any errors in your handler will be caught and raise a 500, so you I<probably> do not need to raise this condition yourself. Anything that is not handled by one of your handlers will raise a 404. The rest is up to you!
174              
175             =back
176              
177             =cut
178              
179             has dispatch => (
180             is => 'rw',
181             isa => 'DispatchMap',
182             required => 1,
183             );
184              
185             =head2 hostname
186              
187             Not to be confused with the parent class's "host" accessor, the hostname has
188             nothing to do with which interface to bind the server to. It is used to fill
189             out Request objects with a full URI (in some cases, the locally known hostname
190             for an interface is NOT what the outside world uses to reach it!
191              
192             =cut
193              
194             has hostname => (is => 'rw');
195              
196             =head2 context
197              
198             Every handler will get passed this object, which is global to the server. It can be anything, but defaults to a hashref. Use this as a quick and dirty stash, and then fix it with something real later.
199              
200             =cut
201              
202             has context => (
203             is => 'rw',
204             default => sub { {} },
205             );
206              
207             =head2 debug
208              
209             If this is set to true, 500 errors will display some debugging information to the browser. Defaults to false.
210              
211             =cut
212              
213             has debug => (
214             is => 'rw',
215             isa => 'Bool',
216             default => 0,
217             );
218              
219             =head2 append_slashes
220              
221             If this is set true (which it is by default), requests for /some/method will be redirected to the /some/method/ handler (if such a handler exists). This is highly recommended, as many user agents start to append slashes if the last component of a path does not have an extension, and it makes things look a little nicer.
222              
223             =cut
224              
225             has append_slashes => (
226             is => 'rw',
227             isa => 'Bool',
228             default => 1,
229             );
230              
231             has request => (
232             is => 'rw',
233             isa => 'HTTP::Server::Simple::Dispatched::Request',
234             );
235              
236             =head1 METHODS
237              
238             =head2 new
239              
240             This is a proper subclass of HTTP::Server::Simple, but the constructor takes all L<ATTRIBUTES> and standard PERLy accessors from the parent class as keyword arguments for convenience.
241              
242             =cut
243              
244             sub new {
245             my $class = shift;
246             my %args;
247              
248             if (@_ == 1) {
249             (ref $_[0] eq 'HASH' and %args = %{$_[0]})
250             or confess 'Single paramaters to new() must be a HASH ref.';
251             } else {
252             %args = @_;
253             }
254              
255             my $server = $class->SUPER::new($args{port});
256              
257             my $meta = $class->meta;
258             my $self = $meta->new_object(__INSTANCE__ => $server, %args);
259              
260             # Moosie constructor params for normal accessors
261             foreach my $key (keys %args) {
262             if(!$meta->has_attribute($key) and my $setter = $self->can($key)) {
263             $setter->($self, $args{$key});
264             }
265             }
266              
267             return $self;
268             }
269              
270             sub headers {
271             my ($self, $args) = @_;
272             for(my $i = 0; $i < @$args; ) {
273             my $header = $args->[$i++];
274             my $value = $args->[$i++];
275             $self->request->header($header => $value);
276             }
277             }
278              
279             before setup => sub {
280             my ($self, %args) = @_;
281              
282             my $uri = URI->new($args{request_uri});
283             $uri->scheme('http');
284             $uri->authority($self->hostname);
285             $uri->port($self->port);
286              
287             $self->request(HTTP::Server::Simple::Dispatched::Request->new(
288             method => $args{method},
289             uri => $uri->canonical,
290             protocol => $args{protocol},
291             handle => $self->stdin_handle,
292             ));
293             };
294              
295             sub handler {
296             my $self = shift;
297             my $request = $self->request;
298              
299             my $response = HTTP::Response->new(200);
300             $response->content_type('text/html');
301             $response->protocol($request->protocol);
302              
303             my $dispatch = $self->dispatch;
304             my $path = uri_unescape($request->uri->path);
305             my $slashes = $self->append_slashes;
306              
307             my $handled = 0;
308              
309             for (my $i = 0; $i < @$dispatch; ) {
310             my $pattern = $dispatch->[$i++];
311             my $handler = $dispatch->[$i++];
312             if ($path =~ $pattern) {
313             eval {
314             $handled = $handler->($response, $request, $self->context);
315             };
316             if (my $err = $@) {
317             $response->headers->clear();
318             $response->code(500);
319             $response->headers->content_type('text/plain');
320             if ($self->debug) {
321             my $reqdump = $self->request->as_string;
322             my $resdump = $response->as_string;
323             my $condump = Dumper($self->context);
324             $response->content(
325             "Handler died: $err\n\n".
326             Devel::StackTrace->new."\n\n".
327             "Request: $reqdump\n".
328             "Response: $resdump\n".
329             "Context:\n$condump"
330             );
331             }
332             else {
333             $response->content("500 - Internal Server Error");
334             }
335             $handled = 1;
336             }
337             }
338             elsif ($slashes && "$path/" =~ $pattern) {
339             $response->code(301);
340             $response->header(Location => "$path/");
341             $handled = 1;
342             }
343             last if $handled;
344             }
345             $response->code(404) unless ($handled);
346             if ($response->code != 200 && $response->code != 500) {
347             $response->headers->content_type('text/plain');
348             $response->content($response->status_line);
349             }
350              
351             unless (defined $response->content_length) {
352             use bytes;
353             $response->content_length(length $response->content);
354             }
355             print $response->as_string;
356             }
357              
358             no Moose;
359             1;
360              
361             =head1 AUTHOR
362              
363             Paul Driver, C<< <frodwith at cpan.org> >>
364              
365             =head1 BUGS
366              
367             Please report any bugs or feature requests to C<bug-http-server-simple-dispatched at rt.cpan.org>, or through the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=HTTP-Server-Simple-Dispatched>. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
368              
369             =head1 CONTRIBUTING
370              
371             The development branch lives at L<http://helios.tapodi.net/~pdriver/Bazaar/HTTP-Server-Simple-Dispatched>. Creating your own branch and sending me the URL is the preferred way to send patches.
372              
373             =head1 ACKNOWLEDGEMENTS
374              
375             The static serve code was adapted from HTTP::Server::Simple::Static - I would have reused, but it didn't do what I wanted at all.
376              
377             As mentioned elsewhere, Django's url dispatching is the inspiration for this module.
378              
379             =head1 SEE ALSO
380              
381             L<HTTP::Response>, L<HTTP::Server::Simple::Dispatched::Request>,
382             L<MIME::Types>, L<Moose>, L<HTTP::Server::Simple::Dispatched>
383              
384             =head1 COPYRIGHT & LICENSE
385              
386             Copyright 2008 Paul Driver, all rights reserved.
387              
388             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.