From: John Napiorkowski Date: Thu, 19 Mar 2015 16:56:08 +0000 (-0500) Subject: docs X-Git-Tag: 5.90089_002~38 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=catagits%2FCatalyst-Runtime.git;a=commitdiff_plain;h=480d94b5f34577816d44fe05389ca5a085179363 docs --- diff --git a/Changes b/Changes index 8fd50ce..0d1c586 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,10 @@ # This file documents the revision history for Perl extension Catalyst. 5.90089_001 - TBA + - New Feature: Type Constraints on Args/CapturArgs. ALlows you to declare + a Moose, MooseX::Types or Type::Tiny named constraint on your Arg or + CaptureArg. + - New top level document on Route matching. (Catalyst::RouteMatching). 5.90084 - 2015-02-23 - Small change to the way body parameters are created in order to prevent @@ -36,7 +40,6 @@ test case to prevent regressions. 5.90080 - 2015-01-09 ->>>>>>> master - Minor documentation corrections - Make the '79 development series stable diff --git a/lib/Catalyst/Controller.pm b/lib/Catalyst/Controller.pm index 860339c..c94f22e 100644 --- a/lib/Catalyst/Controller.pm +++ b/lib/Catalyst/Controller.pm @@ -786,7 +786,29 @@ Like L but scoped under the namespace of the containing controller =head2 CaptureArgs -Please see L +Allowed values for CaptureArgs is a single integer (CaptureArgs(2), meaning two +allowed) or you can declare a L, L or L +named constraint such as CaptureArgs(Int,Str) would require two args with +the first being a Integer and the second a string. You may declare your own +custom type constraints and import them into the controller namespace: + + package MyApp::Controller::Root; + + use Moose; + use MooseX::MethodAttributes; + use MyApp::Types qw/Int/; + + extends 'Catalyst::Controller'; + + sub chain_base :Chained(/) CaptureArgs(1) { } + + sub any_priority_chain :Chained(chain_base) PathPart('') Args(1) { } + + sub int_priority_chain :Chained(chain_base) PathPart('') Args(Int) { } + +See L for more. + +Please see L for more. =head2 ActionClass @@ -836,6 +858,38 @@ When used with L indicates the number of arguments expected in the path. However if no Args value is set, assumed to 'slurp' all remaining path pars under this namespace. +Allowed values for Args is a single integer (Args(2), meaning two allowed) or you +can declare a L, L or L named constraint such +as Args(Int,Str) would require two args with the first being a Integer and the +second a string. You may declare your own custom type constraints and import +them into the controller namespace: + + package MyApp::Controller::Root; + + use Moose; + use MooseX::MethodAttributes; + use MyApp::Types qw/Tuple Int Str StrMatch UserId/; + + extends 'Catalyst::Controller'; + + sub user :Local Args(UserId) { + my ($self, $c, $int) = @_; + } + + sub an_int :Local Args(Int) { + my ($self, $c, $int) = @_; + } + + sub many_ints :Local Args(ArrayRef[Int]) { + my ($self, $c, @ints) = @_; + } + + sub match :Local Args(StrMatch[qr{\d\d-\d\d-\d\d}]) { + my ($self, $c, $int) = @_; + } + +See L for more. + =head2 Consumes('...') Matches the current action against the content-type of the request. Typically diff --git a/lib/Catalyst/DispatchType/Chained.pm b/lib/Catalyst/DispatchType/Chained.pm index 47335a8..2029a6e 100644 --- a/lib/Catalyst/DispatchType/Chained.pm +++ b/lib/Catalyst/DispatchType/Chained.pm @@ -675,6 +675,28 @@ An action that is part of a chain (that is, one that has a C<:Chained> attribute) but has no C<:CaptureArgs> attribute is treated by Catalyst as a chain end. +Allowed values for CaptureArgs is a single integer (CaptureArgs(2), meaning two +allowed) or you can declare a L, L or L +named constraint such as CaptureArgs(Int,Str) would require two args with +the first being a Integer and the second a string. You may declare your own +custom type constraints and import them into the controller namespace: + + package MyApp::Controller::Root; + + use Moose; + use MooseX::MethodAttributes; + use MyApp::Types qw/Int/; + + extends 'Catalyst::Controller'; + + sub chain_base :Chained(/) CaptureArgs(1) { } + + sub any_priority_chain :Chained(chain_base) PathPart('') Args(1) { } + + sub int_priority_chain :Chained(chain_base) PathPart('') Args(Int) { } + +See L for more. + =item Args By default, endpoints receive the rest of the arguments in the path. You diff --git a/lib/Catalyst/RouteMatching.pod b/lib/Catalyst/RouteMatching.pod new file mode 100644 index 0000000..fb86ca0 --- /dev/null +++ b/lib/Catalyst/RouteMatching.pod @@ -0,0 +1,173 @@ +=encoding UTF-8 + +=head1 Name + +Catalyst::RouteMatching - How Catalyst maps an incoming URL to actions in controllers. + +=head1 Description + +This is a WIP document intended to help people understand the logic that L +uses to determine how to match in incoming request to an action (or action chain) +in a controller. + +=head2 Type Constraints in Args and Capture Args + +Beginning in Version 5.90090+ you may use L, L or L +type constraints to futher declare allowed matching for Args or CaptureArgs. Here +is a simple example: + + package MyApp::Controller::User; + + use Moose; + use MooseX::MethodAttributes; + + extends 'Catalyst::Controller'; + + sub find :Path('') Args(Int) { + my ($self, $c, $int) = @_; + } + + __PACKAGE__->meta->make_immutable; + +In this case the incoming request "http://localhost:/user/100" would match the action +C but "http://localhost:/user/not_a_number" would not. You may find declaring +constraints in this manner aids with debuggin, automatic generation of documentation +and reduces the amount of manual checking you might need to do in your actions. For +example if the argument in the example action was going to be used to lookup a row +in a database, if the matching field expected an integer a string might cause a database +exception, prompting you to add additional checking of the argument prior to using it. + +More than one argument may be added by comma separating your type constraint names, for +example: + + sub find :Path('') Args(Int,Int,Str) { + my ($self, $c, $int1, $int2, $str) = @_; + } + +Would require three arguments, an integer, integer and a string. + +=head3 Using type constraints in a controller + +By default L allows all the standard, built-in, named type constraints that come +bundled with L. However it is trivial to create your own Type constraint libraries +and export them to controller that wish to use them. We recommend using L or +L for this. Here is an example using some extended type constraints via +the L library that is packaged with L: + + package MyApp::Controller::User; + + use Moose; + use MooseX::MethodAttributes; + use Types::Standard qw/StrMatch/; + + extends 'Catalyst::Controller'; + + sub looks_like_a_date :Path('') Args(StrMatch[qr{\d\d-\d\d-\d\d}]) { + my ($self, $c, $int) = @_; + } + + __PACKAGE__->meta->make_immutable; + +This would match URLs like "http://localhost/user/11-11-2015" for example. If you've been +missing the old RegExp matching, this can emulate a good chunk of that ability, and more. + +A tutorial on how to make custom type libraries is outside the scope of this document. I'd +recommend looking at the copious documentation in L or in L if +you prefer that system. The author recommends L if you are unsure which to use. + +=head3 Match order when more than one Action matches a path. + +As previously described, L will match 'the longest path', which generally means +that named path / path_parts will take precidence over Args or CaptureArgs. However, what +will happen if two actions match the same path with equal args? For example: + + sub an_int :Path(user) Args(Int) { + } + + sub an_any :Path(user) Args(1) { + } + +In this case L will check actions starting from the LAST one defined. Generally +this means you should put your most specific action rules LAST and your 'catch-alls' first. +In the above example, since Args(1) will match any argument, you will find that that 'an_int' +action NEVER gets hit. You would need to reverse the order: + + sub an_any :Path(user) Args(1) { + } + + sub an_int :Path(user) Args(Int) { + } + +Now requests that match this path would first hit the 'an_int' action, and then the 'an_any' +action, which it likely what you are looking for! + +=head3 Type Constraints and Chained Actions + +Using type constraints in Chained actions works the same as it does for Path and Local or Global +actions. The only difference is that you maybe declare type constraints on CaptureArgs as +well as Args. For Example: + + sub chain_base :Chained(/) CaptureArgs(1) { } + + sub any_priority_chain :Chained(chain_base) PathPart('') Args(1) { } + + sub int_priority_chain :Chained(chain_base) PathPart('') Args(Int) { } + + sub link_any :Chained(chain_base) PathPart('') CaptureArgs(1) { } + + sub any_priority_link_any :Chained(link_any) PathPart('') Args(1) { } + + sub int_priority_link_any :Chained(link_any) PathPart('') Args(Int) { } + + sub link_int :Chained(chain_base) PathPart('') CaptureArgs(Int) { } + + sub any_priority_link :Chained(link_int) PathPart('') Args(1) { } + + sub int_priority_link :Chained(link_int) PathPart('') Args(Int) { } + +These chained actions migth create match tables like the following: + + [debug] Loaded Chained actions: + .----------------------------------------------+----------------------------------------------. + | Path Spec | Private | + +----------------------------------------------+----------------------------------------------+ + | /chain_base/*/* | /chain_base (1) | + | | => /any_priority_chain | + | /chain_base/*/*/* | /chain_base (1) | + | | -> /link_int (1) | + | | => /any_priority_link | + | /chain_base/*/*/* | /chain_base (1) | + | | -> /link_any (1) | + | | => /any_priority_link_any | + | /chain_base/*/* | /chain_base (1) | + | | => /int_priority_chain | + | /chain_base/*/*/* | /chain_base (1) | + | | -> /link_int (1) | + | | => /int_priority_link | + | /chain_base/*/*/* | /chain_base (1) | + | | -> /link_any (1) | + | | => /int_priority_link_any | + '----------------------------------------------+----------------------------------------------' + +As you can see the same general path could be matched by various action chains. In this case +the rule described in the previous section should be followed, which is that L +will start with the last defined action and work upward. For example the action C +would be checked before C. The same applies for actions that are midway links +in a longer chain. In this case C would be checked before C. So as always we +recommend that you place you priority or most constrainted actions last and you least or catch-all +actions first. + +Although this reverse order checking may seen counter intuitive it does have the added benefit that +when inheriting controllers any new actions added would take check precedence over those in your +parent controller or consumed role. + +=head1 Conclusion + + TBD + +=head1 Author + +John Napiorkowski L + +=cut + diff --git a/t/arg_constraints.t b/t/arg_constraints.t index b753e21..57e972b 100644 --- a/t/arg_constraints.t +++ b/t/arg_constraints.t @@ -104,6 +104,12 @@ BEGIN { sub int_priority_chain :Chained(chain_base) PathPart('') Args(Int) { $_[1]->res->body('int_priority_chain') } + sub link_any :Chained(chain_base) PathPart('') CaptureArgs(1) { } + + sub any_priority_link_any :Chained(link_any) PathPart('') Args(1) { $_[1]->res->body('any_priority_link_any') } + + sub int_priority_link_any :Chained(link_any) PathPart('') Args(Int) { $_[1]->res->body('int_priority_link_any') } + sub link_int :Chained(chain_base) PathPart('') CaptureArgs(Int) { } sub any_priority_link :Chained(link_int) PathPart('') Args(1) { $_[1]->res->body('any_priority_link') } @@ -229,4 +235,14 @@ SKIP: { is $res->content, 'int_priority_chain', 'got expected'; } +{ + my $res = request '/chain_base/cap1/a/arg'; + is $res->content, 'any_priority_link_any'; +} + +{ + my $res = request '/chain_base/cap1/a/102'; + is $res->content, 'int_priority_link_any'; +} + done_testing;