Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl Monk, Perl Meditation
 
PerlMonks  

Catalyst - chained actions with empty PathPart

by roman (Monk)
on Feb 26, 2011 at 18:05 UTC ( [id://890325]=perlquestion: print w/replies, xml ) Need Help??

roman has asked for the wisdom of the Perl Monks concerning the following question:

Dear monks,

is there a sane way how to get rid of "unnecessary" part of URL path, while avoiding the matching conflict? Imagine chained URLs (paths) like:

/region/REGION /category/CATEGORY /region/REGION/.... /category/CATEGORY/...
It is easy to implement them as chained URIs
sub load_region : Chained('/') : PathPart('region') : CaptureArgs(1) { ... } sub view_region : Chained('load_region') : PathPart('') : Args(0) { ... } sub load_category : Chained('/') : PathPart('category') : CaptureArgs( +1) { ... } sub view_category : Chained('load_category') : PathPart('') : Args(0) +{ ... }

Now I would like to get rid of region and category path parts. The range of values for REGION and CATEGORY are disjunct and the values are known so I know that /czech deals with region, while /cosmetics is about category. I can strip the PathPart.

sub load_region : Chained('/') : PathPart('') : CaptureArgs(1) { ... } sub view_region : Chained('load_region') : PathPart('') : Args(0) { ... } sub load_region : Chained('/') : PathPart('') : CaptureArgs(1) { ... } sub view_region : Chained('load_region') : PathPart('') : Args(0) { ... }

but it leads to the conflict, because there is no code to resolve whether the first path part is region or category.

Earlier I tried to redefine match on load_region, but unsuccessfully. As far as I understand Catalyst::DispatchType::Chained the $action->match is called only for the last action of the chain, i.e on view_region.

Do you know any way how to enforce matching on captures for intermediate actions of the chain?

I think that this type of URLs is quite common, for example URLs where the first part of path is a language.

I appreciate any advice including why it is unwise to strip path parts.

Replies are listed 'Best First'.
Re: Catalyst - chained actions with empty PathPart
by Anonymous Monk on Feb 27, 2011 at 10:06 UTC

    The simple answer here is: no, chained can't do this, because it represents everything as a tree, so every action has to have a fixed number of levels above it -- and as you say, matching at every level besides the bottom is done purely on PathParts, not via matching.

    What you can do is create a 'default' action (:Path :Args) that will accept any number of args and remove prefixes that it recognizes (calling load_region etc. along the way) and then use the leftover args to redispatch. Unfortunately true redispatch is something else that Catalyst doesn't do very gracefully, but as long as it's in the same controller you should be able to take the first leftover arg as an action name in the current controller and the remainder as args for it, and then forward/detach there.

    -hobbs, who no longer has his perlmonks password and no longer cares enough to retrieve it

      Because I can not do it with Chained dispatch type I created my own dispatch type as a subclass of Catalyst::DispatchType::Chained.

      I tried to modify recurse_match so that intermediate action in chain can refuse their captures. I replaced:

      push(@captures, splice(@parts, 0, $capture_attr->[0]));

      by:

      my @action_captures = splice(@parts, 0, $capture_attr->[0]); next TRY_ACTION if !$self->match_action_captures( $c, $action, \@actio +n_captures ); push(@captures, @action_captures);

      For the captures matching of intermediate action I try to find other (private) action which has the same name + _match_captures suffix. If found the action is called to decide whether the captures are OK or not.

      sub match_action_captures { my ( $this, $c, $action, $action_captures ) = @_; my $mc_action = $c->dispatcher->get_action_by_path( $action->private_path . '_match_captures' ); return $mc_action ? $c->forward( $mc_action, $action_captures ) : +1; }

      In controller it looks like:

      sub load_category_match_captures : Private { my ( $this, $c, $category ) = @_; return scalar grep { $category eq $_ } qw(cosmetics entertainment) +; }

      The Chained dispatch type replacement is a bit cumbersome:

      package TestApp; use Catalyst; # qw(-Debug); sub setup_dispatcher { my $this = shift; my $dispatcher = $this->next::method(@_); $dispatcher->preload_dispatch_types( ['+TestApp::DispatchType::Chained'] ); return $dispatcher; } __PACKAGE__->setup; my $dp = __PACKAGE__->dispatcher->dispatch_types; @$dp = grep { ( ref($_) || $_ ) ne 'Catalyst::DispatchType::Chained'; +} @$dp; 1;

      I tried a few tests and so far it works.

      One of the reasons why I try to stick with Chained are language based URLs I need to implement soon. I decided to put the language at the beginning of the chain without a distinguishing path part.

      /cs/something .... /en/something ....

      To have the language (and region/category) in $c->req->captures has some advantage for uri construction. I have my own version of $c->uri_for_action for chained actions with the same signature $c->my_uri_for_action( $action, $captures?, @arguments, \%params?). The idea is that for a chained $action, I know how many captures it needs. So if I provide less (or none) of them, the captures are supplied from left by those from $c->req->captures. Thus I will almost never needs to add $c->language into $c->uri_for since it is copied from current action.

      The code of new dispatch type class:

      package TestApp::DispatchType::Chained; use Moose; extends 'Catalyst::DispatchType::Chained'; sub recurse_match { my ( $self, $c, $parent, $path_parts ) = @_; my $children = $self->_children_of->{$parent}; return () unless $children; my $best_action; my @captures; TRY: foreach my $try_part (sort { length($b) <=> length($a) } keys %$children) { # $b then $a to try longest part first my @parts = @$path_parts; if (length $try_part) { # test and strip PathPart next TRY unless ($try_part eq join('/', # assemble equal number of parts splice( # and strip them off @parts as w +ell @parts, 0, scalar(@{[split('/', $try_p +art)]}) ))); # @{[]} to avoid split to @_ } my @try_actions = @{$children->{$try_part}}; TRY_ACTION: foreach my $action (@try_actions) { if (my $capture_attr = $action->attributes->{CaptureArgs}) + { # Short-circuit if not enough remaining parts next TRY_ACTION unless @parts >= $capture_attr->[0]; my @captures; my @parts = @parts; # localise # original Catalyst::DispatchType::Chained # push(@captures, splice(@parts, 0, $capture_attr->[0] +)); # /original # modification my @action_captures = splice(@parts, 0, $capture_attr- +>[0]); next TRY_ACTION if !$self->match_action_captures( $c, +$action, \@action_captures ); # strip CaptureArgs into list push(@captures, @action_captures); # /modification # try the remaining parts against children of this act +ion my ($actions, $captures, $action_parts) = $self->recur +se_match( $c, '/'.$action->reverse, + \@parts ); # No best action currently # OR The action has less parts # OR The action has equal parts but less captured data + (ergo more defined) if ($actions && (!$best_action || $#$action_parts < $#{$best_action->{parts}} || ($#$action_parts == $#{$best_action->{parts}} && $#$captures < $#{$best_action->{captures}}))){ $best_action = { actions => [ $action, @$actions ], captures=> [ @captures, @$captures ], parts => $action_parts }; } } else { { local $c->req->{arguments} = [ @{$c->req->args}, @ +parts ]; next TRY_ACTION unless $action->match($c); } my $args_attr = $action->attributes->{Args}->[0]; # No best action currently # OR This one matches with fewer parts left than the c +urrent best action, # And therefore is a better match # OR No parts and this expects 0 # The current best action might also be Args(0), # but we couldn't chose between then anyway so we'l +l take the last seen if (!$best_action || @parts < @{$best_action->{parts}} || (!@parts && $args_attr eq 0)){ $best_action = { actions => [ $action ], captures=> [], parts => \@parts } } } } } return @$best_action{qw/actions captures parts/} if $best_action; return (); } sub match_action_captures { my ( $this, $c, $action, $action_captures ) = @_; my $mc_action = $c->dispatcher->get_action_by_path( $action->private_path . '_match_captures' ); return $mc_action ? $c->forward( $mc_action, $action_captures ) : +1; }
Re: Catalyst - chained actions with empty PathPart
by Anonymous Monk on Feb 27, 2011 at 09:55 UTC
    Having never used Catalyst, I solicited irc://irc.perl.org/#catalyst
    01:37 whidgle day old unanswered question http://perlmonks.com/? +node_id=890325# Catalyst - chained actions with empty PathPart 01:39 hobbs yeah, the answer is don't use Chained for that 01:40 hobbs create a :Args action that will accept any number of + arguments and gets ('region', 'blah', 'category', 'bleh') as @args 01:40 hobbs and takes them two-by-two and sets variables or call +s load_region, load_category, etc. as necessary 01:40 hobbs (using a dispatch table or can or whatever if you li +ke, if you need something that complex, or just "if"... whatever) 01:40 hobbs and then forwards to the real work :) 01:41 whidgle i didn't ask it :) 01:42 hobbs oh, bleh :) 01:42 whidgle "comment on" is the link you want 01:43 hobbs well, I don't particularly want it :-P
      This was followed up by "sorry, misread the whole question" ;)

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://890325]
Approved by Corion
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others making s'mores by the fire in the courtyard of the Monastery: (5)
As of 2024-03-29 06:42 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found