docs
John Napiorkowski [Thu, 19 Mar 2015 16:56:08 +0000 (11:56 -0500)]
Changes
lib/Catalyst/Controller.pm
lib/Catalyst/DispatchType/Chained.pm
lib/Catalyst/RouteMatching.pod [new file with mode: 0644]
t/arg_constraints.t

diff --git a/Changes b/Changes
index 8fd50ce..0d1c586 100644 (file)
--- 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
 
index 860339c..c94f22e 100644 (file)
@@ -786,7 +786,29 @@ Like L</Regex> but scoped under the namespace of the containing controller
 
 =head2 CaptureArgs
 
-Please see L<Catalyst::DispatchType::Chained>
+Allowed values for CaptureArgs is a single integer (CaptureArgs(2), meaning two
+allowed) or you can declare a L<Moose>, L<MooseX::Types> or L<Type::Tiny>
+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<Catalyst::RouteMatching> for more.
+
+Please see L<Catalyst::DispatchType::Chained> for more.
 
 =head2 ActionClass
 
@@ -836,6 +858,38 @@ When used with L</Path> 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<Moose>, L<MooseX::Types> or L<Type::Tiny> 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<Catalyst::RouteMatching> for more.
+
 =head2 Consumes('...')
 
 Matches the current action against the content-type of the request.  Typically
index 47335a8..2029a6e 100644 (file)
@@ -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<Moose>, L<MooseX::Types> or L<Type::Tiny>
+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<Catalyst::RouteMatching> 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 (file)
index 0000000..fb86ca0
--- /dev/null
@@ -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<Catalyst>
+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<Moose>, L<MooseX::Types> or L<Type::Tiny>
+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<find> 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<Catalyst> allows all the standard, built-in, named type constraints that come
+bundled with L<Moose>.  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<Type::Tiny> or
+L<MooseX::Types> for this.  Here is an example using some extended type constraints via
+the L<Types::Standard> library that is packaged with L<Type::Tiny>:
+
+    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<Type::Tiny> or in L<MooseX::Types> if
+you prefer that system.  The author recommends L<Type::Tiny> if you are unsure which to use.
+
+=head3 Match order when more than one Action matches a path.
+
+As previously described, L<Catalyst> 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<Catalyst> 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<Catalyst>
+will start with the last defined action and work upward.  For example the action C<int_priority_chain>
+would be checked before C<any_priority_chain>.  The same applies for actions that are midway links
+in a longer chain.  In this case C<link_int> would be checked before C<link_any>.  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<jjnapiork@cpan.org|email:jjnapiork@cpan.org>
+
+=cut
+
index b753e21..57e972b 100644 (file)
@@ -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;