# 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.90085 - 2015-03-25
+ - Small change to Catalyst::Action to prevent autovivication of Args value (dim1++)
+ - Minor typo fixes (Abraxxa++)
+ - Make sure than when using chained actions and when more than one action
+ matches the same path specification AND has Args(0), that we follow the
+ "in a tie, the last action defined wins" rule. There is a small chance
+ this is a breaking change for you. See Catalyst::Upgrading for more.
+ You may use the application configuration setting "use_chained_args_0_special_case"
+ to disable this new behavior, if you must for back-compat reasons.
+ - Added PATCH HTTP Method action attribute shortcut.
+ - Several new configuration options aimed to give improved backwards compatibility
+ for when your URL query parameters or keywords have non UTF-8 encodings.
+ See Catalyst::Upgrading.
+
5.90084 - 2015-02-23
- Small change to the way body parameters are created in order to prevent
trying to create parameters twice.
__PACKAGE__->_encode_check(Encode::FB_CROAK | Encode::LEAVE_SRC);
# Remember to update this in Catalyst::Runtime as well!
-our $VERSION = '5.90085';
+our $VERSION = '5.90089_001';
$VERSION = eval $VERSION if $VERSION =~ /_/; # numify for warning-free dev releases
sub import {
=item *
+ C<do_not_decode_query>
+
+ If true, then do not try to character decode any wide characters in your
+ request URL query or keywords. Most readings of the relevent specifications
+ suggest these should be UTF-* encoded, which is the default that L<Catalyst>
+ will use, hwoever if you are creating a lot of URLs manually or have external
+ evil clients, this might cause you trouble. If you find the changes introduced
+ in Catalyst version 5.90080+ break some of your query code, you may disable
+ the UTF-8 decoding globally using this configuration.
+
+ This setting takes precedence over C<default_query_encoding> and
+ C<decode_query_using_global_encoding>
+
+ =item *
+
+ C<default_query_encoding>
+
+ By default we decode query and keywords in your request URL using UTF-8, which
+ is our reading of the relevent specifications. This setting allows one to
+ specify a fixed value for how to decode your query. You might need this if
+ you are doing a lot of custom encoding of your URLs and not using UTF-8.
+
+ This setting take precedence over C<decode_query_using_global_encoding>.
+
+ =item *
+
+ C<decode_query_using_global_encoding>
+
+ Setting this to true will default your query decoding to whatever your
+ general global encoding is (the default is UTF-8).
+
+ =item *
+
+ C<use_chained_args_0_special_case>
+
+ In older versions of Catalyst, when more than one action matched the same path
+ AND all those matching actions declared Args(0), we'd break the tie by choosing
+ the first action defined. We now normalized how Args(0) works so that it
+ follows the same rule as Args(N), which is to say when we need to break a tie
+ we choose the LAST action defined. If this breaks your code and you don't
+ have time to update to follow the new normalized approach, you may set this
+ value to true and it will globally revert to the original chaining behavior.
+
+ =item *
+
C<psgi_middleware> - See L<PSGI MIDDLEWARE>.
=item *
sub _parse_DELETE_attr { Method => 'DELETE' }
sub _parse_OPTIONS_attr { Method => 'OPTIONS' }
sub _parse_HEAD_attr { Method => 'HEAD' }
+ sub _parse_PATCH_attr { Method => 'PATCH' }
sub _expand_role_shortname {
my ($self, @shortnames) = @_;
=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
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
@{ $self->_endpoints }
) {
my $args = $endpoint->list_extra_info->{Args};
- my @parts = (defined($args) ? (("*") x $args) : '...');
+ my @parts = (defined($endpoint->attributes->{Args}[0]) ? (("*") x $args) : '...');
my @parents = ();
my $parent = "DUMMY";
my $extra = $self->_list_extra_http_methods($endpoint);
$name = "${extra} ${name}";
}
if (defined(my $cap = $p->list_extra_info->{CaptureArgs})) {
- $name .= ' ('.$cap.')';
+ if($p->has_captures_constraints) {
+ my $tc = join ',', @{$p->captures_constraints};
+ $name .= " ($tc)";
+ } else {
+ $name .= " ($cap)";
+ }
}
if (defined(my $ct = $p->list_extra_info->{Consumes})) {
$name .= ' :'.$ct;
}
push(@rows, [ '', $name ]);
}
+
+ if($endpoint->has_args_constraints) {
+ my $tc = join ',', @{$endpoint->args_constraints};
+ $endpoint .= " ($tc)";
+ } else {
+ $endpoint .= defined($endpoint->attributes->{Args}[0]) ? " ($args)" : " (...)";
+ }
push(@rows, [ '', (@rows ? "=> " : '').($extra ? "$extra " : ''). ($scheme ? "$scheme: ":'')."/${endpoint}". ($consumes ? " :$consumes":"" ) ]);
my @display_parts = map { $_ =~s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg; decode_utf8 $_ } @parts;
$rows[0][0] = join('/', '', @display_parts) || '/';
my @try_actions = @{$children->{$try_part}};
TRY_ACTION: foreach my $action (@try_actions) {
if (my $capture_attr = $action->attributes->{CaptureArgs}) {
- my $capture_count = $capture_attr->[0] || 0;
+ my $capture_count = $action->number_of_captures|| 0;
# Short-circuit if not enough remaining parts
next TRY_ACTION unless @parts >= $capture_count;
push(@captures, splice(@parts, 0, $capture_count));
# check if the action may fit, depending on a given test by the app
- if ($action->can('match_captures')) { next TRY_ACTION unless $action->match_captures($c, \@captures) }
+ next TRY_ACTION unless $action->match_captures($c, \@captures);
# try the remaining parts against children of this action
my ($actions, $captures, $action_parts, $n_pathparts) = $self->recurse_match(
# The current best action might also be Args(0),
# but we couldn't chose between then anyway so we'll take the last seen
- if (!$best_action ||
+ if (
+ !$best_action ||
@parts < @{$best_action->{parts}} ||
- (!@parts && defined($args_attr) && $args_attr eq "0")){
+ (
+ !@parts &&
+ defined($args_attr) &&
+ (
+ $args_attr eq "0" &&
+ (
+ ($c->config->{use_chained_args_0_special_case}||0) ||
+ (
+ exists($best_action->{args_attr}) && defined($best_action->{args_attr}) ?
+ ($best_action->{args_attr} ne 0) : 1
+ )
+ )
+ )
+ )
+ ){
$best_action = {
actions => [ $action ],
captures=> [],
parts => \@parts,
+ args_attr => $args_attr,
n_pathparts => scalar(@pathparts),
};
}
=cut
-sub _check_args_attr {
- my ( $self, $action, $name ) = @_;
-
- return unless exists $action->attributes->{$name};
-
- if (@{$action->attributes->{$name}} > 1) {
- Catalyst::Exception->throw(
- "Multiple $name attributes not supported registering " . $action->reverse()
- );
- }
- my $args = $action->attributes->{$name}->[0];
- if (defined($args) and not (
- Scalar::Util::looks_like_number($args) and
- int($args) == $args and $args >= 0
- )) {
- require Data::Dumper;
- local $Data::Dumper::Terse = 1;
- local $Data::Dumper::Indent = 0;
- $args = Data::Dumper::Dumper($args);
- Catalyst::Exception->throw(
- "Invalid $name($args) for action " . $action->reverse() .
- " (use '$name' or '$name(<number>)')"
- );
- }
-}
-
sub register {
my ( $self, $c, $action ) = @_;
$self->_actions->{'/'.$action->reverse} = $action;
- foreach my $name (qw(Args CaptureArgs)) {
- $self->_check_args_attr($action, $name);
- }
-
if (exists $action->attributes->{Args} and exists $action->attributes->{CaptureArgs}) {
Catalyst::Exception->throw(
"Combining Args and CaptureArgs attributes not supported registering " .
my @captures = @$captures;
my $parent = "DUMMY";
my $curr = $action;
+ # If this is an action chain get the last action in the chain
+ if($curr->can('chain') ) {
+ $curr = ${$curr->chain}[-1];
+ }
while ($curr) {
- if (my $cap = $curr->attributes->{CaptureArgs}) {
- return undef unless @captures >= ($cap->[0]||0); # not enough captures
- if ($cap->[0]) {
- unshift(@parts, splice(@captures, -$cap->[0]));
+ if (my $cap = $curr->number_of_captures) {
+ return undef unless @captures >= $cap; # not enough captures
+ if ($cap) {
+ unshift(@parts, splice(@captures, -$cap));
}
}
if (my $pp = $curr->attributes->{PathPart}) {
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
--package Catalyst::Runtime;
use strict;
use warnings;
# Remember to update this in Catalyst as well!
-our $VERSION = '5.90085';
+our $VERSION = '5.90089_001';
$VERSION = eval $VERSION if $VERSION =~ /_/; # numify for warning-free dev releases
=head1 NAME