File Coverage

blib/lib/Catalyst/Plugin/URI.pm
Criterion Covered Total %
statement 16 22 72.7
branch 9 26 34.6
condition 3 5 60.0
subroutine 3 3 100.0
pod n/a
total 31 56 55.3


line stmt bran cond sub pod time code
1             package Catalyst::Plugin::URI;
2              
3 1     1   3632 use Moo::Role;
  1         17210  
  1         4  
4 1     1   3111 use Scalar::Util ();
  1         3  
  1         972  
5              
6             requires 'uri_for';
7              
8             our $VERSION = '0.004';
9              
10             my $uri_v1 = sub {
11             my ($c, $path, @args) = @_;
12            
13             # already is an $action
14             if(Scalar::Util::blessed($path) && $path->isa('Catalyst::Action')) {
15             return $c->uri_for($path, @args);
16             }
17            
18             # Hard error if the spec looks wrong...
19             die "$path is not a string" unless ref \$path eq 'SCALAR';
20             die "$path is not a controller.action specification" unless $path=~m/^(.*)\.(.+)$/;
21            
22             die "$1 is not a controller"
23             unless my $controller = $c->controller($1||'');
24            
25             die "$2 is not an action for controller ${\$controller->component_name}"
26             unless my $action = $controller->action_for($2);
27            
28             return $c->uri_for($action, @args);
29             };
30              
31             my $uri_v2 = sub {
32 6     6   4604 my ($c, $action_proto, @args) = @_;
33              
34             # already is an $action
35 6 50 33     28 if(Scalar::Util::blessed($action_proto) && $action_proto->isa('Catalyst::Action')) {
36 0         0 return $c->uri_for($action_proto, @args);
37             }
38              
39             # Hard error if the spec looks wrong...
40 6 50       84 die "$action_proto is not a string" unless ref \$action_proto eq 'SCALAR';
41              
42 6         15 my $action;
43 6 100       83 if($action_proto =~/^\/?#/) {
    50          
    0          
    0          
44 1 50       6 die "$action_proto is not a named action"
45             unless $action = $c->dispatcher->get_action_by_path($action_proto);
46             } elsif($action_proto=~m/^(.*)\:(.+)$/) {
47 5 50 100     40 die "$1 is not a controller"
48             unless my $controller = $c->controller($1||'');
49 5 50       404 die "$2 is not an action for controller ${\$controller->component_name}"
  0         0  
50             unless $action = $controller->action_for($2);
51             } elsif($action_proto =~/\//) {
52 0 0       0 my $path = $action_proto=~m/^\// ? $action_proto : $c->controller->action_for($action_proto)->private_path;
53 0 0       0 die "$action_proto is not a full or relative private action path" unless $path;
54 0 0       0 die "$path is not a private path" unless $action = $c->dispatcher->get_action_by_path($path);
55             } elsif($action = $c->controller->action_for($action_proto)) {
56             # Noop
57             } else {
58             # Fallback to static
59 0         0 $action = $action_proto;
60             }
61 6 50       1172 die "We can't create a URI from $action with the given arguements"
62             unless my $uri = $c->uri_for($action, @args);
63              
64 6         5183 return $uri;
65             };
66              
67             *uri = $uri_v2;
68              
69             after 'setup_finalize', sub {
70             my ($c) = @_;
71             if (my $config = $c->config->{'Plugin::URI'}) {
72             if($config->{use_v1}) {
73             *uri = $uri_v1;
74             } else {
75             *uri = $uri_v2;
76             }
77             } else {
78             *uri = $uri_v2;
79             }
80             };
81              
82             after 'setup_actions', sub {
83             my ($c) = @_;
84             my %action_hash = %{$c->dispatcher->_action_hash||+{}};
85             foreach my $key (keys %action_hash) {
86             if(my ($name) = @{$action_hash{$key}->attributes->{Name}||[]}) {
87             die "You can only name endpoint actions on a chain"
88             if defined$action_hash{$key}->attributes->{CaptureArgs};
89             die "Named action '$name' is already defined"
90             if $c->dispatcher->_action_hash->{"/#$name"};
91             $c->dispatcher->_action_hash->{"/#$name"} = $action_hash{$key};
92             }
93             }
94             };
95              
96             foreach my $method(qw/detach forward visit go/) {
97             around $method, sub {
98             my ($orig, $c, $action_proto, @args) = @_;
99             my $action;
100             if(defined($action_proto) && $action_proto =~/^\/?#/) {
101             die "$action_proto is not a named action"
102             unless $action = $c->dispatcher->get_action_by_path($action_proto);
103             } else {
104             $action = $action_proto;
105             }
106              
107             $c->$orig($action, @args);
108             };
109             }
110              
111             1;
112              
113             =head1 NAME
114              
115             Catalyst::Plugin::URI - Yet another sugar plugin for $c->uri_for
116              
117             =head1 SYNOPSIS
118              
119             Use the plugin in your application class:
120              
121             package MyApp;
122             use Catalyst 'URI';
123              
124             MyApp->setup;
125              
126             Then you can use it in your controllers:
127              
128             package MyApp::Controller::Example;
129              
130             use base 'Catalyst::Controller';
131              
132             sub make_a_url :Local {
133             my ($self, $c) = @_;
134             my $url = $c->uri("$controller.$action", \@args, \%query, \$fragment);
135             }
136              
137             This is just a shortcut with stronger error messages for:
138              
139             sub make_a_url :Local {
140             my ($self, $c) = @_;
141             my $url = $c->url_for(
142             $c->controller($controller)->action_for($action),
143             \@args, \%query, \$fragment);
144             }
145              
146             =head1 DESCRIPTION
147              
148             B<NOTE> Starting with version '0.003' I changed that way this works. If you want
149             or need the old API for backcompatibility please set the following configuration
150             flag:
151              
152             MyApp->config('Plugin::URI' => { use_v1 => 1 });
153              
154             Currently if you want to create a URL to a controller's action properly the formal
155             syntax is rather verbose:
156              
157             my $url = $c->uri(
158             $c->controller($controller)->action_for($action),
159             \@args, \%query, \$fragment);
160              
161              
162             Which is verbose enough that it probably encourages people to do the wrong thing
163             and use a hard coded link path. This might later bite you if you need to change
164             your controllers and URL hierarchy.
165              
166             Also, this can lead to weird error messages that don't make if clear that your
167             $controller and $action are actually wrong. This plugin is an attempt to both
168             make the proper formal syntax a bit more tidy and to deliver harder error messages
169             if you get the names wrong.
170              
171             =head1 METHODS
172              
173             This plugin adds the following methods to your context
174              
175             =head2 uri
176              
177             Example:
178              
179             $c->uri("$controller:$action", \@parts, \%query, \$fragment);
180              
181             This is a sugar method which works the same as:
182              
183             my $url = $c->uri_for(
184             $c->controller($controller)->action_for($action),
185             \@args, \%query, \$fragment);
186              
187             Just a bit shorter, and also we check to make sure the $controller and
188             $action actually exist (and raise a hard fail if they don't with an error
189             message that is I think more clear than the longer version.
190              
191             You can also use a 'relative' specification for the action, which assumes
192             the current controller. For example:
193              
194             $c->uri(":$action", \@parts, \%query, \$fragment);
195              
196             Basically the same as:
197              
198             my $url = $c->uri_for(
199             $self->action_for($action),
200             \@args, \%query, \$fragment);
201              
202             We also support a corrected version of what 'uri_for_action' meant to achieve:
203              
204             $c->uri("$action", @args);
205              
206             Basically the same as:
207              
208             my $url = $c->uri_for($self->action_for($action), @args);
209              
210             Where the $action string is the full or relative (to the current controller) private
211             name of the action. Please note this does support path traversal with '..' so the
212             following means "create a URL to an action in the controller namespace above the
213             current one":
214              
215             my $url = $c->uri("../foo"); # $c->uri($self->action_for("../foo"));
216              
217             Experimentally we support named actions so that you can specify a link with a custom
218             name:
219              
220             sub name_action :Local Args(0) Name(hi) {
221             my ($self, $c) = @_;
222             # rest of action
223             }
224              
225             my $url = $c->uri("#hi");
226              
227             This allows you to specify the action by its name from any controller. We don't
228             allow you to use the same name twice, and we also throw an exception if you attempt
229             to add a name to an intermediate action in a chain of actions (you can only name
230             an endpoint).
231              
232              
233             Lastly For ease of use if the first argument is an action object we just pass it
234             down to 'uri_for'. That way you should be able to use this method for all types
235             of URL creation.
236              
237             =head1 OTHER SIMILAR OPTIONS
238              
239             L<Catalyst> offers a second way to make URLs that use the action private
240             name, the 'uri_for_action' method. However this suffers from a bug where
241             'path/action' and '/path/action' work the same (no support for relative
242             actions). Also this doesn't give you a very good error message if the action
243             private path does not exist, leading to difficult debugging issues sometimes.
244             Lastly I just personally prefer to look up an action via $controller->action_for(...)
245             over the private path, which is somewhat dependent on controller namespace
246             information that you might change.
247              
248             Prior art on CPAN doesn't seem to solve issues that I think actually exist (
249             for example older versions of L<Catalyst> required that you specify capture
250             args from args in a Chained action, there's plugins to address that but that
251             was fixed in core L<Catalyst> quite a while ago.) This plugin exists merely as
252             sugar over the formal syntax and tries to do nothing else fancy.
253              
254             =head1 AUTHOR
255              
256             John Napiorkowski L<email:jjnapiork@cpan.org>
257            
258             =head1 SEE ALSO
259            
260             L<Catalyst>
261              
262             =head1 COPYRIGHT & LICENSE
263            
264             Copyright 2015, John Napiorkowski L<email:jjnapiork@cpan.org>
265            
266             This library is free software; you can redistribute it and/or modify it under
267             the same terms as Perl itself.
268            
269             =cut