import "builder" into the right namespace - thanks, gbhat!
[catagits/Web-Simple.git] / lib / Web / Simple / Application.pm
CommitLineData
5c33dda5 1package Web::Simple::Application;
2
975048a1 3use Scalar::Util 'weaken';
4
8bd060f4 5use Moo;
5c33dda5 6
876e62e1 7has 'config' => (
8 is => 'ro',
9 default => sub {
10 my ($self) = @_;
11 +{ $self->default_config }
12 },
13 trigger => sub {
14 my ($self, $value) = @_;
15 my %default = $self->default_config;
16 my @not = grep !exists $value->{$_}, keys %default;
17 @{$value}{@not} = @default{@not};
18 }
19);
5c33dda5 20
445b3ea0 21sub default_config { () }
39119082 22
445b3ea0 23has '_dispatcher' => (is => 'lazy');
5c33dda5 24
445b3ea0 25sub _build__dispatcher {
26 my $self = shift;
27 require Web::Dispatch;
445b3ea0 28 my $final = $self->_build_final_dispatcher;
b5b4423b 29
30 # We need to weaken both the copy of $self that the
31 # app parameter will close over and the copy that'll
32 # be passed through as a node argument.
33 #
34 # To ensure that this doesn't then result in us being
35 # DESTROYed unexpectedly early, our to_psgi_app method
36 # closes back over $self
37
38 weaken($self);
1f8cad5e 39 my %dispatch_args = (
e5250d96 40 dispatch_app => sub { $self->dispatch_request(@_), $final },
1f8cad5e 41 dispatch_object => $self
445b3ea0 42 );
1f8cad5e 43 weaken($dispatch_args{dispatch_object});
44 Web::Dispatch->new(%dispatch_args);
5c33dda5 45}
46
3583ca04 47sub _build_final_dispatcher {
4ed4fb42 48 [ 404, [ 'Content-type', 'text/plain' ], [ 'Not found' ] ]
5c33dda5 49}
50
5c33dda5 51sub run_if_script {
b9e047ef 52 # ->to_psgi_app is true for require() but also works for plackup
445b3ea0 53 return $_[0]->to_psgi_app if caller(1);
e27ab5c5 54 my $self = ref($_[0]) ? $_[0] : $_[0]->new;
5c33dda5 55 $self->run(@_);
56}
57
913a9cf9 58sub _run_cgi {
5c33dda5 59 my $self = shift;
2bc99ccd 60 require Plack::Handler::CGI;
61 Plack::Handler::CGI->new->run($self->to_psgi_app);
d3a61609 62}
63
e27ab5c5 64sub _run_fcgi {
65 my $self = shift;
2bc99ccd 66 require Plack::Handler::FCGI;
67 Plack::Handler::FCGI->new->run($self->to_psgi_app);
e27ab5c5 68}
69
445b3ea0 70sub to_psgi_app {
bc57805c 71 my $self = ref($_[0]) ? $_[0] : $_[0]->new;
b5b4423b 72 my $app = $self->_dispatcher->to_app;
73
74 # Close over $self to keep $self alive even though
75 # we weakened the copies the dispatcher has; the
76 # if 0 causes the ops to be optimised away to
77 # minimise the performance impact and avoid void
78 # context warnings while still doing the closing
79 # over part. As Mithaldu said: "Gnarly." ...
80
81 return sub { $self if 0; goto &$app; };
5c33dda5 82}
83
913a9cf9 84sub run {
85 my $self = shift;
66350cd3 86 if (
87 $ENV{PHP_FCGI_CHILDREN} || $ENV{FCGI_ROLE} || $ENV{FCGI_SOCKET_PATH}
7b930ebb 88 || ( -S STDIN && !$ENV{GATEWAY_INTERFACE} )
89 # If STDIN is a socket, almost certainly FastCGI, except for mod_cgid
66350cd3 90 ) {
e27ab5c5 91 return $self->_run_fcgi;
92 } elsif ($ENV{GATEWAY_INTERFACE}) {
2514ad17 93 return $self->_run_cgi;
913a9cf9 94 }
5b8f03a7 95 unless (@ARGV && $ARGV[0] =~ m{(^[A-Z/])|\@}) {
db2899c3 96 return $self->_run_cli(@ARGV);
d104fb1d 97 }
98
4ba6d891 99 my @args = @ARGV;
913a9cf9 100
5b8f03a7 101 unshift(@args, 'GET') if $args[0] !~ /^[A-Z]/;
4ba6d891 102
c1db3355 103 $self->_run_cli_test_request(@args);
4ba6d891 104}
105
c1db3355 106sub _test_request_spec_to_http_request {
4ba6d891 107 my ($self, $method, $path, @rest) = @_;
108
c1db3355 109 # if it's a reference, assume a request object
110 return $method if ref($method);
913a9cf9 111
8c3623e2 112 if ($path =~ s/^(.*?)\@//) {
113 my $basic = $1;
114 require MIME::Base64;
115 unshift @rest, 'Authorization:', 'Basic '.MIME::Base64::encode($basic);
116 }
117
4ba6d891 118 my $request = HTTP::Request->new($method => $path);
c1db3355 119
82bc2f9c 120 my @params;
121
122 while (my ($header, $value) = splice(@rest, 0, 2)) {
123 unless ($header =~ s/:$//) {
124 push @params, $header, $value;
125 }
15928515 126 $header =~ s/_/-/g;
127 if ($header eq 'Content') {
128 $request->content($value);
129 } else {
130 $request->headers->push_header($header, $value);
131 }
82bc2f9c 132 }
133
9f3d2dd9 134 if (($method eq 'POST' or $method eq 'PUT') and @params) {
4ba6d891 135 my $content = do {
136 require URI;
137 my $url = URI->new('http:');
9f3d2dd9 138 $url->query_form(@params);
4ba6d891 139 $url->query;
140 };
141 $request->header('Content-Type' => 'application/x-www-form-urlencoded');
142 $request->header('Content-Length' => length($content));
143 $request->content($content);
144 }
c1db3355 145
146 return $request;
147}
148
149sub run_test_request {
150 my ($self, @req) = @_;
151
5b8f03a7 152 require HTTP::Request;
153
c1db3355 154 require Plack::Test;
155
156 my $request = $self->_test_request_spec_to_http_request(@req);
157
4ba6d891 158 Plack::Test::test_psgi(
c1db3355 159 $self->to_psgi_app, sub { shift->($request) }
4ba6d891 160 );
c1db3355 161}
162
163sub _run_cli_test_request {
164 my ($self, @req) = @_;
165 my $response = $self->run_test_request(@req);
166
167 binmode(STDOUT); binmode(STDERR); # for win32
168
baabba33 169 print STDERR $response->status_line."\n";
170 print STDERR $response->headers_as_string("\n")."\n";
f9d0d382 171 my $content = $response->content;
172 $content .= "\n" if length($content) and $content !~ /\n\z/;
173 print STDOUT $content if $content;
913a9cf9 174}
175
d104fb1d 176sub _run_cli {
177 my $self = shift;
178 die $self->_cli_usage;
179}
180
181sub _cli_usage {
182 "To run this script in CGI test mode, pass a URL path beginning with /:\n".
183 "\n".
184 " $0 /some/path\n".
185 " $0 /\n"
186}
187
5c33dda5 1881;
32d29dcd 189
190=head1 NAME
191
6a4808bf 192Web::Simple::Application - A base class for your Web-Simple application
32d29dcd 193
194=head1 DESCRIPTION
195
196This is a base class for your L<Web::Simple> application. You probably don't
197need to construct this class yourself, since L<Web::Simple> does the 'heavy
198lifting' for you in that regards.
199
200=head1 METHODS
201
6a4808bf 202This class exposes the following public methods.
32d29dcd 203
204=head2 default_config
205
6a4808bf 206Merges with the C<config> initializer to provide configuration information for
207your application. For example:
32d29dcd 208
209 sub default_config {
210 (
211 title => 'Bloggery',
212 posts_dir => $FindBin::Bin.'/posts',
213 );
214 }
215
6a4808bf 216Now, the C<config> attribute of C<$self> will be set to a HashRef
32d29dcd 217containing keys 'title' and 'posts_dir'.
218
12b3e9a3 219The keys from default_config are merged into any config supplied, so
220if you construct your application like:
6a4808bf 221
12b3e9a3 222 MyWebSimpleApp::Web->new(
223 config => { title => 'Spoon', environment => 'dev' }
224 )
6a4808bf 225
12b3e9a3 226then C<config> will contain:
6a4808bf 227
12b3e9a3 228 {
229 title => 'Spoon',
230 posts_dir => '/path/to/myapp/posts',
231 environment => 'dev'
232 }
32d29dcd 233
12b3e9a3 234=head2 run_if_script
32d29dcd 235
12b3e9a3 236The run_if_script method is designed to be used at the end of the script
237or .pm file where your application class is defined - for example:
32d29dcd 238
239 ## my_web_simple_app.pl
240 #!/usr/bin/env perl
241 use Web::Simple 'HelloWorld';
242
243 {
244 package HelloWorld;
245
246 sub dispatch_request {
247 sub (GET) {
248 [ 200, [ 'Content-type', 'text/plain' ], [ 'Hello world!' ] ]
249 },
250 sub () {
251 [ 405, [ 'Content-type', 'text/plain' ], [ 'Method not allowed' ] ]
252 }
253 }
254 }
255
256 HelloWorld->run_if_script;
257
12b3e9a3 258This returns a true value, so your file is now valid as a module - so
6a4808bf 259
12b3e9a3 260 require 'my_web_simple_app.pl';
6a4808bf 261
12b3e9a3 262 my $hw = HelloWorld->new;
6a4808bf 263
12b3e9a3 264will work fine (and you can rename it to lib/HelloWorld.pm later to make it
265a real use-able module).
6a4808bf 266
12b3e9a3 267However, it detects if it's being run as a script (via testing $0) and if
268so attempts to do the right thing.
32d29dcd 269
12b3e9a3 270If run under a CGI environment, your application will execute as a CGI.
32d29dcd 271
12b3e9a3 272If run under a FastCGI environment, your application will execute as a
273FastCGI process (this works both for dynamic shared-hosting-style FastCGI
274and for apache FastCgiServer style setups).
32d29dcd 275
12b3e9a3 276If run from the commandline with a URL path, it runs a GET request against
277that path -
32d29dcd 278
12b3e9a3 279 $ perl -Ilib examples/hello-world/hello-world.cgi /
280 200 OK
281 Content-Type: text/plain
282
283 Hello world!
32d29dcd 284
15928515 285You can also provide a method name -
286
287 $ perl -Ilib examples/hello-world/hello-world.cgi POST /
288 405 Method Not Allowed
289 Content-Type: text/plain
290
291 Method not allowed
292
293For a POST or PUT request, pairs on the command line will be treated
294as form variables. For any request, pairs on the command line ending in :
295are treated as headers, and 'Content:' will set the request body -
296
297 $ ./myapp POST / Accept: text/html form_field_name form_field_value
298
299 $ ./myapp POST / Content-Type: text/json Content: '{ "json": "here" }'
300
301The body of the response is sent to STDOUT and the headers to STDERR, so
302
303 $ ./myapp GET / >index.html
304
305will generally do the right thing.
306
8c3623e2 307To send basic authentication credentials, use user:pass@ syntax -
308
309 $ ./myapp GET bob:secret@/protected/path
310
12b3e9a3 311Additionally, you can treat the file as though it were a standard PSGI
312application file (*.psgi). For example you can start up up with C<plackup>
313
314 plackup my_web_simple_app.pl
32d29dcd 315
12b3e9a3 316or C<starman>
32d29dcd 317
12b3e9a3 318 starman my_web_simple_app.pl
319
320=head2 to_psgi_app
321
322This method is called by L</run_if_script> to create the L<PSGI> app coderef
323for use via L<Plack> and L<plackup>. If you want to globally add middleware,
324you can override this method:
6a4808bf 325
326 use Web::Simple 'HelloWorld';
6a4808bf 327
328 {
329 package HelloWorld;
ea54c010 330 use Plack::Builder;
6a4808bf 331
332 around 'to_psgi_app', sub {
333 my ($orig, $self) = (shift, shift);
334 my $app = $self->$orig(@_);
335 builder {
336 enable ...; ## whatever middleware you want
337 $app;
338 };
339 };
340 }
341
12b3e9a3 342This method can also be used to mount a Web::Simple application within
343a separate C<*.psgi> file -
344
345 use strictures 1;
346 use Plack::Builder;
347 use WSApp;
348 use AnotherWSApp;
349
350 builder {
351 mount '/' => WSApp->to_psgi_app;
352 mount '/another' => AnotherWSApp->to_psgi_app;
353 };
354
355This method can be called as a class method, in which case it implicitly
356calls ->new, or as an object method ... in which case it doesn't.
32d29dcd 357
358=head2 run
359
30e2c525 360Used for running your application under stand-alone CGI and FCGI modes.
32d29dcd 361
ca30a017 362I should document this more extensively but run_if_script will call it when
363you need it, so don't worry about it too much.
364
365=head2 run_test_request
366
15928515 367 my $res = $app->run_test_request(GET => '/' => %headers);
ca30a017 368
15928515 369 my $res = $app->run_test_request(POST => '/' => %headers_or_form);
ca30a017 370
371 my $res = $app->run_test_request($http_request);
372
373Accepts either an L<HTTP::Request> object or ($method, $path) and runs that
374request against the application, returning an L<HTTP::Response> object.
375
376If the HTTP method is POST or PUT, then a series of pairs can be passed after
377this to create a form style message body. If you need to test an upload, then
378create an L<HTTP::Request> object by hand or use the C<POST> subroutine
379provided by L<HTTP::Request::Common>.
380
8c3623e2 381If you prefix the URL with 'user:pass@' this will be converted into
382an Authorization header for HTTP basic auth:
383
384 my $res = $app->run_test_request(
385 GET => 'bob:secret@/protected/resource'
386 );
387
15928515 388If pairs are passed where the key ends in :, it is instead treated as a
389headers, so:
390
391 my $res = $app->run_test_request(
392 POST => '/',
393 'Accept:' => 'text/html',
394 some_form_key => 'value'
395 );
396
397will do what you expect. You can also pass a special key of Content: to
398set the request body:
399
400 my $res = $app->run_test_request(
401 POST => '/',
402 'Content-Type:' => 'text/json',
403 'Content:' => '{ "json": "here" }',
404 );
405
7e103e8e 406=head1 AUTHORS
32d29dcd 407
7e103e8e 408See L<Web::Simple> for authors.
32d29dcd 409
7e103e8e 410=head1 COPYRIGHT AND LICENSE
32d29dcd 411
7e103e8e 412See L<Web::Simple> for the copyright and license.
32d29dcd 413
414=cut