From: Arthur Axel 'fREW' Schmidt Date: Fri, 27 Nov 2009 17:04:18 +0000 (-0600) Subject: Merge branch 'master' of catagits@git.shadowcat.co.uk:Web-Simple X-Git-Tag: v0.003~27 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=7c6c7743ea9c839b2433c4a80a092cc7a432603c;hp=d7c9ce98d5ce5c2c38d93a02b8c0c84822d757a1;p=catagits%2FWeb-Simple.git Merge branch 'master' of catagits@git.shadowcat.co.uk:Web-Simple --- diff --git a/lib/Web/Simple.pm b/lib/Web/Simple.pm index 88a8b26..60bccc9 100644 --- a/lib/Web/Simple.pm +++ b/lib/Web/Simple.pm @@ -175,121 +175,52 @@ so that perl will not attempt to load the application again even if is encountered in other code. -=head1 EXPORTED SUBROUTINES - -=head2 default_config - - default_config( - one_key => 'foo', - another_key => 'bar', - ); - - ... - - $self->config->{one_key} # 'foo' - -This creates the default configuration for the application, by creating a - - sub _default_config { - return (one_key => 'foo', another_key => 'bar'); - } - -in the application namespace when executed. Note that this means that -you should only run default_config once - calling it a second time will -cause an exception to be thrown. - -=head2 dispatch - - dispatch { - sub (GET) { - [ 200, [ 'Content-type', 'text/plain' ], [ 'Hello world!' ] ] - }, - sub () { - [ 405, [ 'Content-type', 'text/plain' ], [ 'Method not allowed' ] ] - } - }; - -The dispatch subroutine calls NameOfApplication->_setup_dispatcher with -the return value of the block passed to it, which then creates your Web::Simple -application's dispatcher from these subs. The prototype of each subroutine -is expected to be a Web::Simple dispatch specification (see -L below for more details), and the body of the -subroutine is the code to execute if the specification matches. - -Each dispatcher is given the dispatcher constructed from the next subroutine -returned as its next dispatcher, except for the final subroutine, which -is given the return value of NameOfApplication->_build_final_dispatcher -as its next dispatcher (by default this returns a 500 error response). - -See L below for details on how the Web::Simple dispatch -system uses the return values of these subroutines to determine how to -continue, alter or abort dispatch. - -Note that _setup_dispatcher creates a - - sub _dispatcher { - return ; - } - -method in your class so as with default_config, calling dispatch a second time -will result in an exception. - -=head2 response_filter - - response_filter { - # Hide errors from the user because we hates them, preciousss - if (ref($_[1]) eq 'ARRAY' && $_[1]->[0] == 500) { - $_[1] = [ 200, @{$_[1]}[1..$#{$_[1]}] ]; - } - return $_[1]; - }; - -The response_filter subroutine is designed for use inside dispatch subroutines. - -It creates and returns a special dispatcher that always matches, and calls -the block passed to it as a filter on the result of running the rest of the -current dispatch chain. - -Thus the filter above runs further dispatch as normal, but if the result of -dispatch is a 500 (Internal Server Error) response, changes this to a 200 (OK) -response without altering the headers or body. - -=head2 redispatch_to - - redispatch_to '/other/url'; - -The redispatch_to subroutine is designed for use inside dispatch subroutines. - -It creates and returns a special dispatcher that always matches, and instead -of continuing dispatch re-delegates it to the start of the dispatch process, -but with the path of the request altered to the supplied URL. - -Thus if you receive a POST to '/some/url' and return a redipstch to -'/other/url', the dispatch behaviour will be exactly as if the same POST -request had been made to '/other/url' instead. - -=head2 subdispatch - - subdispatch sub (/user/*/) { - my $u = $self->user($_[1]); - [ - sub (GET) { $u }, - sub (DELETE) { $u->delete }, - ] - } - -The subdispatch subroutine is designed for use in dispatcher construction. - -It creates a dispatcher which, if it matches, treats its return value not -as a final value but an arrayref of dispatch specifications such as could -be passed to the dispatch subroutine itself. These are turned into a dispatcher -which is then invoked. Any changes the match makes to the request are in -scope for this inner dispatcher only - so if the initial match is a -destructive one like .html the full path will be restored if the -subdispatch fails. - =head1 DISPATCH STRATEGY +=head2 Examples + + dispatch { + # matches: GET /user/1.htm?show_details=1 + # GET /user/1.htm + sub (GET + /user/* + ?show_details~ + .htm|.html|.xhtml) { + shift; my ($user_id, $show_details) = @_; + ... + }, + # matches: POST /user?username=frew + # POST /user?username=mst&first_name=matt&last_name=trout + sub (POST + /user + ?username=&*) { + shift; my ($username, $misc_params) = @_; + ... + }, + # matches: DELETE /user/1/friend/2 + sub (DELETE + /user/*/friend/*) { + shift; my ($user_id, $friend_id) = @_; + ... + }, + # matches: PUT /user/1?first_name=Matt&last_name=Trout + sub (PUT + /user/* + ?first_name~&last_name~) { + shift; my ($user_id, $first_name, $last_name) = @_; + ... + }, + sub (/user/*/...) { + my $user_id = $_[1]; + subdispatch sub { + [ + # matches: PUT /user/1/role/1 + sub (PUT + /role/*) { + my $role_id = $_[1]; + ... + }, + # matches: DELETE /user/1/role/1 + sub (DELETE + /role/*) { + my $role_id = shift; + ... + }, + ]; + } + }, + } + =head2 Description of the dispatcher object Web::Simple::Dispatcher objects have three components: @@ -558,6 +489,119 @@ from subroutine prototypes, so this is equivalent to sub (GET+/user/*) { +=head1 EXPORTED SUBROUTINES + +=head2 default_config + + default_config( + one_key => 'foo', + another_key => 'bar', + ); + + ... + + $self->config->{one_key} # 'foo' + +This creates the default configuration for the application, by creating a + + sub _default_config { + return (one_key => 'foo', another_key => 'bar'); + } + +in the application namespace when executed. Note that this means that +you should only run default_config once - calling it a second time will +cause an exception to be thrown. + +=head2 dispatch + + dispatch { + sub (GET) { + [ 200, [ 'Content-type', 'text/plain' ], [ 'Hello world!' ] ] + }, + sub () { + [ 405, [ 'Content-type', 'text/plain' ], [ 'Method not allowed' ] ] + } + }; + +The dispatch subroutine calls NameOfApplication->_setup_dispatcher with +the return value of the block passed to it, which then creates your Web::Simple +application's dispatcher from these subs. The prototype of each subroutine +is expected to be a Web::Simple dispatch specification (see +L below for more details), and the body of the +subroutine is the code to execute if the specification matches. + +Each dispatcher is given the dispatcher constructed from the next subroutine +returned as its next dispatcher, except for the final subroutine, which +is given the return value of NameOfApplication->_build_final_dispatcher +as its next dispatcher (by default this returns a 500 error response). + +See L below for details on how the Web::Simple dispatch +system uses the return values of these subroutines to determine how to +continue, alter or abort dispatch. + +Note that _setup_dispatcher creates a + + sub _dispatcher { + return ; + } + +method in your class so as with default_config, calling dispatch a second time +will result in an exception. + +=head2 response_filter + + response_filter { + # Hide errors from the user because we hates them, preciousss + if (ref($_[1]) eq 'ARRAY' && $_[1]->[0] == 500) { + $_[1] = [ 200, @{$_[1]}[1..$#{$_[1]}] ]; + } + return $_[1]; + }; + +The response_filter subroutine is designed for use inside dispatch subroutines. + +It creates and returns a special dispatcher that always matches, and calls +the block passed to it as a filter on the result of running the rest of the +current dispatch chain. + +Thus the filter above runs further dispatch as normal, but if the result of +dispatch is a 500 (Internal Server Error) response, changes this to a 200 (OK) +response without altering the headers or body. + +=head2 redispatch_to + + redispatch_to '/other/url'; + +The redispatch_to subroutine is designed for use inside dispatch subroutines. + +It creates and returns a special dispatcher that always matches, and instead +of continuing dispatch re-delegates it to the start of the dispatch process, +but with the path of the request altered to the supplied URL. + +Thus if you receive a POST to '/some/url' and return a redipstch to +'/other/url', the dispatch behaviour will be exactly as if the same POST +request had been made to '/other/url' instead. + +=head2 subdispatch + + subdispatch sub (/user/*/) { + my $u = $self->user($_[1]); + [ + sub (GET) { $u }, + sub (DELETE) { $u->delete }, + ] + } + +The subdispatch subroutine is designed for use in dispatcher construction. + +It creates a dispatcher which, if it matches, treats its return value not +as a final value but an arrayref of dispatch specifications such as could +be passed to the dispatch subroutine itself. These are turned into a dispatcher +which is then invoked. Any changes the match makes to the request are in +scope for this inner dispatcher only - so if the initial match is a +destructive one like .html the full path will be restored if the +subdispatch fails. + =head1 CHANGES BETWEEN RELEASES =head2 Changes since Antiquated Perl @@ -568,7 +612,7 @@ from subroutine prototypes, so this is equivalent to This is a pure rename; a global search and replace should fix it. -=item * dispatch [] changed to dispatch [] +=item * dispatch [] changed to dispatch {} Simply changing diff --git a/t/dispatch_parser.t b/t/dispatch_parser.t index f8e7e06..4608893 100644 --- a/t/dispatch_parser.t +++ b/t/dispatch_parser.t @@ -7,161 +7,188 @@ use Web::Simple::DispatchParser; my $dp = Web::Simple::DispatchParser->new; -my $get = $dp->parse_dispatch_specification('GET'); - -is_deeply( - [ $get->({ REQUEST_METHOD => 'GET' }) ], - [ {} ], - 'GET matches' -); - -is_deeply( - [ $get->({ REQUEST_METHOD => 'POST' }) ], - [], - 'POST does not match' -); +{ + my $get = $dp->parse_dispatch_specification('GET'); + + is_deeply( + [ $get->({ REQUEST_METHOD => 'GET' }) ], + [ {} ], + 'GET matches' + ); + + is_deeply( + [ $get->({ REQUEST_METHOD => 'POST' }) ], + [], + 'POST does not match' + ); +} ok( !eval { $dp->parse_dispatch_specification('GET POST'); 1; }, "Don't yet allow two methods" ); -my $html = $dp->parse_dispatch_specification('.html'); - -is_deeply( - [ $html->({ PATH_INFO => '/foo/bar.html' }) ], - [ { PATH_INFO => '/foo/bar' } ], - '.html matches' -); - -is_deeply( - [ $html->({ PATH_INFO => '/foo/bar.xml' }) ], - [], - '.xml does not match .html' -); - -my $any_ext = $dp->parse_dispatch_specification('.*'); - -is_deeply( - [ $any_ext->({ PATH_INFO => '/foo/bar.html' }) ], - [ { PATH_INFO => '/foo/bar' }, 'html' ], - '.html matches .* and extension returned' -); - -is_deeply( - [ $any_ext->({ PATH_INFO => '/foo/bar' }) ], - [], - 'no extension does not match .*' -); - - -my $slash = $dp->parse_dispatch_specification('/'); - -is_deeply( - [ $slash->({ PATH_INFO => '/' }) ], - [ {} ], - '/ matches /' -); - -is_deeply( - [ $slash->({ PATH_INFO => '/foo' }) ], - [ ], - '/foo does not match /' -); - -my $post = $dp->parse_dispatch_specification('/post/*'); - -is_deeply( - [ $post->({ PATH_INFO => '/post/one' }) ], - [ {}, 'one' ], - '/post/one parses out one' -); - -is_deeply( - [ $post->({ PATH_INFO => '/post/one/' }) ], - [], - '/post/one/ does not match' -); - -my $combi = $dp->parse_dispatch_specification('GET+/post/*'); - -is_deeply( - [ $combi->({ PATH_INFO => '/post/one', REQUEST_METHOD => 'GET' }) ], - [ {}, 'one' ], - '/post/one parses out one' -); - -is_deeply( - [ $combi->({ PATH_INFO => '/post/one/', REQUEST_METHOD => 'GET' }) ], - [], - '/post/one/ does not match' -); - -is_deeply( - [ $combi->({ PATH_INFO => '/post/one', REQUEST_METHOD => 'POST' }) ], - [], - 'POST /post/one does not match' -); - -my $or = $dp->parse_dispatch_specification('GET|POST'); - -foreach my $meth (qw(GET POST)) { - - is_deeply( - [ $or->({ REQUEST_METHOD => $meth }) ], - [ {} ], - 'GET|POST matches method '.$meth - ); +{ + my $html = $dp->parse_dispatch_specification('.html'); + + is_deeply( + [ $html->({ PATH_INFO => '/foo/bar.html' }) ], + [ { PATH_INFO => '/foo/bar' } ], + '.html matches' + ); + + is_deeply( + [ $html->({ PATH_INFO => '/foo/bar.xml' }) ], + [], + '.xml does not match .html' + ); } -is_deeply( - [ $or->({ REQUEST_METHOD => 'PUT' }) ], - [], - 'GET|POST does not match PUT' -); - -$or = $dp->parse_dispatch_specification('GET|POST|DELETE'); +{ + my $any_ext = $dp->parse_dispatch_specification('.*'); + + is_deeply( + [ $any_ext->({ PATH_INFO => '/foo/bar.html' }) ], + [ { PATH_INFO => '/foo/bar' }, 'html' ], + '.html matches .* and extension returned' + ); + + is_deeply( + [ $any_ext->({ PATH_INFO => '/foo/bar' }) ], + [], + 'no extension does not match .*' + ); +} -foreach my $meth (qw(GET POST DELETE)) { +{ + my $slash = $dp->parse_dispatch_specification('/'); + + is_deeply( + [ $slash->({ PATH_INFO => '/' }) ], + [ {} ], + '/ matches /' + ); + + is_deeply( + [ $slash->({ PATH_INFO => '/foo' }) ], + [ ], + '/foo does not match /' + ); +} - is_deeply( - [ $or->({ REQUEST_METHOD => $meth }) ], - [ {} ], - 'GET|POST|DELETE matches method '.$meth - ); +{ + my $post = $dp->parse_dispatch_specification('/post/*'); + + is_deeply( + [ $post->({ PATH_INFO => '/post/one' }) ], + [ {}, 'one' ], + '/post/one parses out one' + ); + + is_deeply( + [ $post->({ PATH_INFO => '/post/one/' }) ], + [], + '/post/one/ does not match' + ); } -is_deeply( - [ $or->({ REQUEST_METHOD => 'PUT' }) ], - [], - 'GET|POST|DELETE does not match PUT' -); +{ + my $combi = $dp->parse_dispatch_specification('GET+/post/*'); + + is_deeply( + [ $combi->({ PATH_INFO => '/post/one', REQUEST_METHOD => 'GET' }) ], + [ {}, 'one' ], + '/post/one parses out one' + ); + + is_deeply( + [ $combi->({ PATH_INFO => '/post/one/', REQUEST_METHOD => 'GET' }) ], + [], + '/post/one/ does not match' + ); + + is_deeply( + [ $combi->({ PATH_INFO => '/post/one', REQUEST_METHOD => 'POST' }) ], + [], + 'POST /post/one does not match' + ); +} -my $nest = $dp->parse_dispatch_specification('(GET+/foo)|POST'); +{ + my $combi = $dp->parse_dispatch_specification('?foo='); -is_deeply( - [ $nest->({ PATH_INFO => '/foo', REQUEST_METHOD => 'GET' }) ], - [ {} ], - '(GET+/foo)|POST matches GET /foo' -); + is_deeply( + [ $combi->({ PATH_INFO => '/?foo=' }) ], + [ {}, 'one' ], + '/post/one parses out one' + ); +} -is_deeply( - [ $nest->({ PATH_INFO => '/bar', REQUEST_METHOD => 'GET' }) ], - [], - '(GET+/foo)|POST does not match GET /bar' -); +{ + my $or = $dp->parse_dispatch_specification('GET|POST'); + + foreach my $meth (qw(GET POST)) { + + is_deeply( + [ $or->({ REQUEST_METHOD => $meth }) ], + [ {} ], + 'GET|POST matches method '.$meth + ); + } + + is_deeply( + [ $or->({ REQUEST_METHOD => 'PUT' }) ], + [], + 'GET|POST does not match PUT' + ); +} -is_deeply( - [ $nest->({ PATH_INFO => '/bar', REQUEST_METHOD => 'POST' }) ], - [ {} ], - '(GET+/foo)|POST matches POST /bar' -); +{ + my $or = $dp->parse_dispatch_specification('GET|POST|DELETE'); + + foreach my $meth (qw(GET POST DELETE)) { + + is_deeply( + [ $or->({ REQUEST_METHOD => $meth }) ], + [ {} ], + 'GET|POST|DELETE matches method '.$meth + ); + } + + is_deeply( + [ $or->({ REQUEST_METHOD => 'PUT' }) ], + [], + 'GET|POST|DELETE does not match PUT' + ); +} -is_deeply( - [ $nest->({ PATH_INFO => '/foo', REQUEST_METHOD => 'PUT' }) ], - [], - '(GET+/foo)|POST does not match PUT /foo' -); +{ + my $nest = $dp->parse_dispatch_specification('(GET+/foo)|POST'); + + is_deeply( + [ $nest->({ PATH_INFO => '/foo', REQUEST_METHOD => 'GET' }) ], + [ {} ], + '(GET+/foo)|POST matches GET /foo' + ); + + is_deeply( + [ $nest->({ PATH_INFO => '/bar', REQUEST_METHOD => 'GET' }) ], + [], + '(GET+/foo)|POST does not match GET /bar' + ); + + is_deeply( + [ $nest->({ PATH_INFO => '/bar', REQUEST_METHOD => 'POST' }) ], + [ {} ], + '(GET+/foo)|POST matches POST /bar' + ); + + is_deeply( + [ $nest->({ PATH_INFO => '/foo', REQUEST_METHOD => 'PUT' }) ], + [], + '(GET+/foo)|POST does not match PUT /foo' + ); +} { local $@; @@ -181,45 +208,49 @@ is_deeply( ); } -my $not = $dp->parse_dispatch_specification('!.html+.*'); - -is_deeply( - [ $not->({ PATH_INFO => '/foo.xml' }) ], - [ { PATH_INFO => '/foo' }, 'xml' ], - '!.html+.* matches /foo.xml' -); - -is_deeply( - [ $not->({ PATH_INFO => '/foo.html' }) ], - [], - '!.html+.* does not match /foo.html' -); - -is_deeply( - [ $not->({ PATH_INFO => '/foo' }) ], - [], - '!.html+.* does not match /foo' -); - -my $sub = $dp->parse_dispatch_specification('/foo/*/...'); - -is_deeply( - [ $sub->({ PATH_INFO => '/foo/1/bar' }) ], - [ { PATH_INFO => '/bar' }, 1 ], - '/foo/*/... matches /foo/1/bar and strips to /bar' -); - -is_deeply( - [ $sub->({ PATH_INFO => '/foo/1/' }) ], - [ { PATH_INFO => '/' }, 1 ], - '/foo/*/... matches /foo/1/bar and strips to /' -); +{ + my $not = $dp->parse_dispatch_specification('!.html+.*'); + + is_deeply( + [ $not->({ PATH_INFO => '/foo.xml' }) ], + [ { PATH_INFO => '/foo' }, 'xml' ], + '!.html+.* matches /foo.xml' + ); + + is_deeply( + [ $not->({ PATH_INFO => '/foo.html' }) ], + [], + '!.html+.* does not match /foo.html' + ); + + is_deeply( + [ $not->({ PATH_INFO => '/foo' }) ], + [], + '!.html+.* does not match /foo' + ); +} -is_deeply( - [ $sub->({ PATH_INFO => '/foo/1' }) ], - [], - '/foo/*/... does not match /foo/1 (no trailing /)' -); +{ + my $sub = $dp->parse_dispatch_specification('/foo/*/...'); + + is_deeply( + [ $sub->({ PATH_INFO => '/foo/1/bar' }) ], + [ { PATH_INFO => '/bar' }, 1 ], + '/foo/*/... matches /foo/1/bar and strips to /bar' + ); + + is_deeply( + [ $sub->({ PATH_INFO => '/foo/1/' }) ], + [ { PATH_INFO => '/' }, 1 ], + '/foo/*/... matches /foo/1/bar and strips to /' + ); + + is_deeply( + [ $sub->({ PATH_INFO => '/foo/1' }) ], + [], + '/foo/*/... does not match /foo/1 (no trailing /)' + ); +} my $q = 'foo=FOO&bar=BAR1&baz=one+two&quux=QUUX1&quux=QUUX2' .'&bar=BAR2&quux=QUUX3&evil=%2F';