File Coverage

blib/lib/Catalyst/ControllerRole/At.pm
Criterion Covered Total %
statement 56 56 100.0
branch 32 34 94.1
condition 8 8 100.0
subroutine 2 2 100.0
pod n/a
total 98 100 98.0


line stmt bran cond sub pod time code
1             package Catalyst::ControllerRole::At;
2              
3 1     1   1558 use Moose::Role;
  1         1  
  1         6  
4             our $VERSION = '0.005';
5              
6             sub _parse_At_attr {
7 17     17   392802 my ($self, $app, $action_subname, $value) = @_;
8 17         33 my ($chained, $path_part, $arg_type, $args, %extra_proto) = ('/','','Args',0, ());
9            
10 17         68 my @controller_path_parts = split('/', $self->path_prefix($app));
11 17         1327 my @parent_controller_path_parts = @controller_path_parts;
12 17         22 my $affix = pop @parent_controller_path_parts;
13              
14 17   100     140 my %expansions = (
15             '$up' => '/' . join('/', @parent_controller_path_parts),
16             '$parent' => '/' . join('/', @parent_controller_path_parts, $action_subname),
17             '$name' => $action_subname,
18             '$controller' => '/' . join('/', @controller_path_parts),
19             '$action' => '/' . join('/', @controller_path_parts, $action_subname),
20             '$affix' => '/' . ($affix||''),
21             );
22              
23 17         21 $value = $value . '';
24 17         71 my ($path, $query) = ($value=~/^([^?]*)\??(.*)$/);
25 17 100 100     67 my (@path_parts) = map { $expansions{$_} ? $expansions{$_} :$_ } split('/', ($path||''));
  38         76  
26              
27 17         19 my @arg_proto;
28             my @named_fields;
29              
30 17 100       23 if($query) {
31 1         7 my @q = ($query=~m/{(.+?)}/g);
32 1         2 $extra_proto{QueryParam} = \@q;
33 1         2 foreach my $q (@q) {
34 2         4 my ($q_part, $type) = split(':', $q);
35 2 50       5 if(defined($q_part)) {
36 2 100       9 if($q_part=~m/=/) {
37 1         5 ($q_part) = split('=', $q_part); # Discard any=default
38             }
39 2         5 $q_part=~s/^[!?]//;
40             $extra_proto{Field} = $extra_proto{Field} ?
41 2 100       11 "$extra_proto{Field},$q_part=>\$query{$q_part}" : "$q_part=>\$query{$q_part}"
42             }
43             }
44             }
45              
46 17 100 100     39 if(($path_parts[-1]||'') eq '...') {
47 3         4 $arg_type = 'CaptureArgs';
48 3         8 pop @path_parts;
49             }
50              
51 17   100     73 while(my ($spec) = (($path_parts[-1]||'') =~m/^{(.*)}$/)) {
52 17 100       25 if($spec) {
53 15         22 my ($name, $constraint) = split(':', $spec);
54 15 100       29 unshift @arg_proto, $constraint if $constraint;
55 15 100       18 if($name) {
56 11 100       14 if($name eq '*') {
57 2         3 $args = undef;
58             } else {
59 9         10 unshift @named_fields, $name;
60             }
61             } else {
62 4         5 unshift @named_fields, undef;
63             }
64             }
65 17 100       29 $args++ if defined $args;
66             } continue {
67 17         48 pop @path_parts;
68             }
69              
70             {
71 17         15 my $cnt = 0;
  17         13  
72 17         22 foreach my $name (@named_fields) {
73 13 100       17 if(defined($name)) {
74             $extra_proto{Field} = $extra_proto{Field} ?
75 9 100       31 "$extra_proto{Field},$name=>\$args[$cnt]" : "$name=>\$args[$cnt]"
76             }
77 13         15 $cnt++;
78             }
79             }
80              
81 17 100       15 if(
82 7         46 my ($key, $value) = map { $_ =~ /^(.*?)(?:\(\s*(.+?)\s*\))?$/ } grep { $_ =~m/^Via\(.+\)$/ }
  24         1073  
83 17 50       49 @{$self->meta->get_method($action_subname)->attributes||[]})
84             {
85 7 100       14 $chained = join '/', grep { defined $_ } map { $expansions{$_} ? $expansions{$_} : $_ } split('\/',$value);
  9         15  
  9         23  
86 7         12 $chained =~s[//][/]g;
87             }
88              
89 17         39 $path_part = join('/', @path_parts);
90 17         28 $path_part =~s/^\///;
91              
92 17 100       96 my %attributes = (
93             Chained => $chained,
94             PathPart => $path_part,
95             Does => [qw/NamedFields QueryParameter/],
96             $arg_type => (@arg_proto ? (join(',',@arg_proto)) : $args),
97             %extra_proto,
98             );
99              
100 17         110 return %attributes;
101             }
102              
103             1;
104              
105             =head1 NAME
106              
107             Catalyst::ControllerRole::At - A new approach to building Catalyst actions
108              
109             =head1 SYNOPSIS
110              
111             package MyApp::Controller::User;
112              
113             use Moose;
114             use MooseX::MethodAttributes;
115             use Types::Standard qw/Int Str/;
116              
117             extends 'Catalyst::Controller';
118             with 'Catalyst::ControllerRole::At';
119              
120             # Define your actions, for example:
121            
122             sub global :At(/global/{}/{}) { ... } # http://localhost/global/$arg/$arg
123              
124             sub list :At($action?{q:Str}) { ... } # http://localhost/user/list?q=$string
125              
126             sub find :At($controller/{id:Int}) { ... } # http://localhost/user/$integer
127              
128             __PACKAGE__->meta->make_immutable;
129              
130             =head1 DESCRIPTION
131              
132             The way L<Catalyst> uses method attributes to annote a subroutine with meta
133             information used to map that action to an incoming request has sometimes been difficult
134             for newcomers to the framework. Partly this is due to how the system evolved and was
135             augmented, with more care towards backwards compatibility (for example with L<Maypole>, its
136             architectural anscestor) than with designing a forward system that is easy to grasp.
137             Additionally aspects of the system such as chained dispatch are very useful in the
138             hands of an expert but the interface leaves a lot to be desired. For example it is
139             possible to craft actions that mix chaining syntax with 'classic' syntax in ways that
140             are confusing. And having more than one way to do the same thing without clear and
141             obvious benefits is confusing to newcomers.
142              
143             Lastly, the core L<Catalyst::Controller> syntax has confusing defaults that are not readily guessed.
144             For example do you know the difference (if any) between Args and Args()? Or the difference
145             between Path, Path(''), and Path()? In many cases defaults are applied that were not
146             intended and things that you might think are the same turn out to have different effects. All
147             this conspires to worsen the learning curve.
148              
149             This role defines an alternative syntax that we hope is easier to understand and for the most
150             part eliminates defaults and guessed intentions. It only defines two method attributes, "At()"
151             and "Via()", which have no defaults and one of which is always required. It also smooths
152             over differences between 'classic' route matching using :Local and :Path and the newer
153             syntax based on Chaining by providing a single approach that bridges between the two
154             styles. One can mix and match the two without being required to learn a new syntax or to
155             rearchitect the system.
156              
157             The "At()" syntax more closely resembles the type of URL you are trying to match, which should
158             make code creation and maintainance easier by reducing the mental mismatch that happens with
159             the core syntax.
160              
161             Ultimately this ControllerRole is an attempt to layer some sugar on top of the existing
162             interface with the hope to establishing a normalized, easy approach that doesn't have the
163             learning curve or confusion of the existing system.
164              
165             I also recommend reading L<Catalyst::RouteMatching> for general notes and details on
166             how dispatching and matching works.
167              
168             =head1 URL Templating
169              
170             The following are examples and specification for how to map a URL to an action or to
171             a chain of actions in L<Catalyst>. All examples assume the application is running at
172             the root of your website domain (https://localhost/, not https://localhost/somepath)
173              
174             =head2 Matching a Literal Path
175              
176             The action 'global_path' will respond to 'https://localhost/foo/bar/baz'.
177              
178             package MyApp::Controller::Example;
179              
180             use Moose;
181             use MooseX::MethodAttributes;
182              
183             extends 'Catalyst::Controller';
184             with 'Catalyst::ControllerRole::At';
185              
186             sub global_path :At(/foo/bar/baz) { ... }
187              
188             __PACKAGE__->meta->make_immutable;
189              
190             The main two parts are consuming the role c< with 'Catalyst::ControllerRole::At'>
191             and using the C<At> method attribute. This attribute can only appear once in your
192             action and should be string that matches a specification as to be described in the
193             following examples.
194              
195             =head2 Arguments in a Path specification
196              
197             Often you wish to parameterize your URL template such that instead of matching a full
198             literal path, you may instead place slots for placeholders, which get passed to the
199             action during a request. For example:
200              
201             package MyApp::Controller::Example;
202              
203             use Moose;
204             use MooseX::MethodAttributes;
205              
206             extends 'Catalyst::Controller';
207             with 'Catalyst::ControllerRole::At';
208              
209             sub args :At(/example/{}) {
210             my ($self, $c, $arg) = @_;
211             }
212              
213             __PACKAGE__->meta->make_immutable;
214              
215             In the above controller we'd match a URL like 'https://localhost/example/100' and
216             'https://localhost/example/whatever'. The parameterized argument is passed as '$arg'
217             into the action when a request is matched.
218              
219             You may have as many argument placeholders as you wish, or you may specific an open
220             ended number of placeholders:
221              
222             sub arg2 :At(/example/{}/{}) { ... } # https://localhost/example/foo/bar
223             sub args :At(/example/{*} { ... } # https://localhost/example/1/2/3/4/...
224              
225             In this case action 'arg2' matches its path with 2 arguments, while 'args' will match
226             'any number of arguments', subject to operating system limitations.
227              
228             B<NOTE> Since the open ended argument specification can catch lots of URLs, this type
229             of argument specification is run as a special 'low priorty' match. For example (using
230             the above two actions) should the request be 'https://localhost/example/foo/bar', then
231             the first action 'arg2' would match since its a better match for that request given it
232             has a more constrained specification. In general I recommend using '{*}' sparingly.
233              
234             B<NOTE> Placeholder must come after path part literals or expansion variables as discussed
235             below. For example "At(/bar/{}/bar)" is not valid. This type of match is possible with
236             chained actions (see more examples below).
237              
238             =head2 Naming your Arguments
239              
240             You may name your argument placeholders. If you do so you can access your argument
241             placeholder values via the %_ hash. For example:
242              
243             sub args :At(/example/{id}) {
244             my ($self, $c, $id) = @_;
245             $c->response->body("The requested ID is $_{id}");
246             }
247              
248             Note that regardless of whether you name your arguments or not, they will get passed to
249             your actions at request via @_, as in core L<Catalyst>. So in the above example '$id'
250             is equal to '$_{id}'. You may use whichever makes the most sense for your task, or
251             standardize a project on one form or the other. You might also find naming the arguments
252             to be a useful form of documentation.
253              
254             =head2 Type constraints on your Arguments
255              
256             You may leverage the built in support for applying type constraints on your arguments:
257              
258             package MyApp::Controller::Example;
259              
260             use Moose;
261             use MooseX::MethodAttributes;
262             use Types::Standard qw/Int/;
263              
264             extends 'Catalyst::Controller';
265             with 'Catalyst::ControllerRole::At';
266              
267             sub args :At(/example/{id:Int}) {
268             my ($self, $c, $id) = @_;
269             }
270              
271             __PACKAGE__->meta->make_immutable;
272              
273             Would match 'http://localhost/example/100' but not 'http://localhost/example/string'
274              
275             All the same rules that apply to L<Catalyst> regarding use of type constraints apply. Most
276             importantly you must remember to inport your type constraints, as in the above example. You
277             should consider reviewing L<Catalyst::RouteMatching> for more general help.
278              
279             You may declare a type constraint on an argument but not name it, as in the following
280             example:
281              
282             sub args :At(/example/{:Int}) {
283             my ($self, $c, $id) = @_;
284             }
285              
286             Note the ':' prepended to the type constraint name is NOT optional.
287              
288             B<NOTE> Using type constraints in your route matching can have performance implications.
289              
290             B<NOTE> If you have more than one argument placeholder and you apply a type constraint to
291             one, you must apply constraints to all. You may use an open type constraint like C<Any>
292             as defined in L<Types::Standard> for placeholders where you don't care what the value is. For
293             example:
294              
295             use Types::Standard qw/Any Int/;
296              
297             sub args :At(/example/{:Any}/{:Int}) {
298             my ($self, $c, $id) = @_;
299             }
300              
301             =head2 Expansion Variables in your Path
302              
303             Generally you would prefer not to hardcode the full path of your actions, as in the
304             examples given so far. General Catalyst best practice is to have your actions live
305             under the namespace of the controller in which they are defined. That makes things
306             more organized and easier to find as your application grows in complexity. In order
307             to make this and other common action template patterns easier, we support the following
308             variable expansions in your URL template specification:
309              
310             $controller: Your controller namespace (as an absolute path)
311             $action: The action namespace (same as $controller/$name)
312             $up: The namespace of the controller containing this controller
313             $name The name of your action (the subroutine name)
314             $affix: The last part of the controller namespace.
315              
316             For example if your controller is 'MyApp::Controller::User::Details' then:
317              
318             $controller => /user/details
319             $up => /user
320             $affix => /details
321              
322             And if 'MyApp::Controller::User::Details' contained an action like:
323              
324             sub list :At() { ... }
325              
326             then:
327              
328             $name => /list
329             $action => /user/details/list
330              
331             You use these variable expansions the same way as literal paths:
332              
333             package MyApp::Controller::Example;
334              
335             use Moose;
336             use MooseX::MethodAttributes;
337             use Types::Standard qw/Int/;
338              
339             extends 'Catalyst::Controller';
340             with 'Catalyst::ControllerRole::At';
341              
342             sub args :At($controller/{id:Int}) {
343             my ($self, $c, $id) = @_;
344             }
345              
346             sub list :At($action) { ... }
347              
348             __PACKAGE__->meta->make_immutable;
349              
350             In this example the action 'args' would match 'https://localhost/example/100' (with '100' being
351             considered an argument) while action 'list' would match 'https::/localhost/example/list'.
352              
353             You can use expansion variables in your base controllers or controller roles to more
354             easily make shared actions.
355              
356             B<NOTE> Your controller namespace is typically based on its package name, unless you
357             have overridden it by setting an alternative in the configuation value 'namespace', or
358             your have in some way overridden the logic that produces a namespace. The default
359             behavior is to produce a namespace like the following:
360              
361             package MyApp::Controller::User => /user
362             package MyApp::Controller::User::name => /user/name
363              
364             Changing the way a controller defines its namespace will also change how actions that are
365             defined in that controller defines thier namespaces.
366              
367             B<NOTE> WHen using expansions, you should not place a '/' at the start of your
368             template URI.
369              
370             =head2 Matching GET parameters
371              
372             You can match GET (query) parameters in your URL template definitions:
373              
374             package MyApp::Controller::Example;
375              
376             use Moose;
377             use MooseX::MethodAttributes;
378             use Types::Standard qw/Int Str/;
379              
380             extends 'Catalyst::Controller';
381             with 'Catalyst::ControllerRole::At';
382              
383             sub query :At($action?{name:Str}{age:Int}) {
384             my ($self, $c, $id) = @_;
385             }
386              
387             __PACKAGE__->meta->make_immutable;
388              
389             This would match 'https://example/query?name=john;age=47'.
390              
391             Your query keys will appear in the %_ in the same way as all your named arguments.
392              
393             You do not need to use a type constraint on the query parameters. If you do not do so
394             all that is required is that the requested query parameters exist.
395              
396             This uses the ActionRole L<Catalyst::ActionRole::QueryParameter> under the hood, which
397             you may wish to review for more details.
398              
399             =head2 Chaining Actions inside a Controller
400              
401             L<Catalyst> action chaining allows you to spread the logic associated with a given URL
402             across a set of actions which all are responsible for handling a part of the URL
403             template. The idea is to allow you to better decompose your logic to promote clarity
404             and reuse. However the built-in syntax for declaring action chains is difficult for
405             many people to use. Here's how you do it with L<Catalyst::ControllerRole::At>
406              
407             Starting a Chain of actions is straightforward. you just add '/...' to the end of your
408             path specification. This is to indicate that the action expects more parts 'to follow'.
409             For example:
410              
411             package MyApp::Controller::Example;
412              
413             use Moose;
414             use MooseX::MethodAttributes;
415             use Types::Standard qw/Int Str/;
416              
417             extends 'Catalyst::Controller';
418             with 'Catalyst::ControllerRole::At';
419              
420             sub init :At($controller/...) { ... }
421            
422             __PACKAGE__->meta->make_immutable;
423              
424             The action 'init' starts a new chain of actions and declares the first part of the
425             definition, 'https://localhost/example/...'. You continue a chain in the same way,
426             but you need to specify the parent action that is being continued using the 'Via'
427             attribute. You terminate a chain when you define an action that doesn't declare '...'
428             as the last path. For example:
429              
430             sub init :At($controller/...) {
431             my ($self, $c) = @_;
432             }
433              
434             sub next :Via(init) At({}/...) {
435             my ($self, $c, $arg) = @_;
436             }
437              
438             sub last :Via(next) At({}) {
439             my ($self, $c, $arg) = @_;
440             }
441              
442             This defines an action chain with three 'stops' which matches a URL like (for example)
443             'https://localhost/$controller/arg1/arg2'. Each action will get executed for the matching
444             part, and will get arguments as defined in their match specification.
445              
446             B<NOTE> The 'Via' attribute must contain a value.
447            
448             When chaining you can use (or not) any mix of type constraints on your arguments, named
449             arguments, and query parameter matching. Here's a full example:
450              
451             package MyApp::Controller::Example;
452              
453             use Moose;
454             use MooseX::MethodAttributes;
455             use Types::Standard qw/Int/;
456              
457             extends 'Catalyst::Controller';
458             with 'Catalyst::ControllerRole::At';
459              
460             sub init :At($controller/...) { ... }
461              
462             sub next :Via(init) At({id:Int}/...) {
463             my ($self, $c, $int_id) = @_;
464             }
465              
466             sub last :Via(next) At({id:Int}?{q}) {
467             my ($self, $c, $int_id) = @_;
468             }
469              
470             __PACKAGE__->meta->make_immutable;
471              
472             =head2 Actions in a Chain with no match template
473              
474             Sometimes for the purposes of organizing code you will have an action that is a
475             midpoint in a chain that does not match any part of a URL template. For that
476             case you can omit the path and argument match specification. For example:
477              
478             package MyApp::Controller::Example;
479              
480             use Moose;
481             use MooseX::MethodAttributes;
482             use Types::Standard qw/Int/;
483              
484             extends 'Catalyst::Controller';
485             with 'Catalyst::ControllerRole::At';
486              
487             sub init :At($controller/...) { ... }
488              
489             sub middle :Via(init) At(...) {
490             my ($self, $c) = @_;
491             }
492              
493             sub last :Via(next) At({id:Int}) {
494             my ($self, $c, $id) = @_;
495             }
496              
497             __PACKAGE__->meta->make_immutable;
498              
499             This will match a URL like 'https://localhost/example/100'.
500              
501             B<NOTE> If you declare a Via but not At, this is an error. You must
502             always provide an At(), even in the case of a terminal action with no
503             match parts of it own. For example:
504              
505             package MyApp::Controller::Example;
506              
507             use Moose;
508             use MooseX::MethodAttributes;
509              
510             extends 'Catalyst::Controller';
511             with 'Catalyst::ControllerRole::At';
512              
513             sub first :At($controller/...) { ... }
514              
515             sub second :Via(first) At(...) {
516             my ($self, $c) = @_;
517             }
518              
519             sub third :Via(second) At(...) {
520             my ($self, $c) = @_;
521             }
522              
523             sub last :Via(third) At() {
524             my ($self, $c, $id) = @_;
525             }
526              
527             __PACKAGE__->meta->make_immutable;
528              
529             This creates a chained action that matches 'http://localhost/example' but calls
530             each of the three actions in the chain in order. Although it might seem odd to
531             create an action that is not connected to a path part of a URL request, you might find
532             cases where this results in well factored and reusable controllers.
533              
534             B<NOTE> For the purposes of executing code, we treat 'At' and 'At()' as the same. However
535             We highly recommend At() as a best practice since it more clearly represents the idea
536             of 'no match template'.
537              
538             =head2 Chaining Actions across Controllers
539              
540             The method attributes 'Via()' contains a pointer to the action being continued. In
541             standard practice this is almost always the name of an action in the same controller
542             as the one declaring it. This could be said to be a 'relative' (as in relative to
543             the current controller) action. However you don't have to use a relative name. You
544             can use any action's absolute private name, as long as it is an action that declares itself
545             to be a link in a chain.
546              
547             However in practice it is not alway a good idea to spread your chained acions across
548             across controllers in a manner that is not easy to follow. We recommend you try
549             to limit youself to chains that follow the controller hierarchy, which should be
550             easier for your code maintainers.
551              
552             For this common, best practice case when you are continuing your chained actions across
553             controllers, following a controller hierarchy, we provide some template expansions you can
554             use in the 'Via' attribute. These are useful to enforce this best practice as well as
555             promote reusability by decoupling hard coded private action namespaces from your controller.
556              
557             $up: The controller whose namespace contains the current controller
558             $name The name of the current actions subroutine
559             $parent: Expands to $up/$subname
560              
561             For example:
562              
563             package MyApp::Controller::ThingsTodo;
564              
565             use Moose;
566             use MooseX::MethodAttributes;
567              
568             extends 'Catalyst::Controller';
569             with 'Catalyst::ControllerRole::At';
570              
571             sub init :At($controller/...) {
572             my ($self, $c) = @_;
573             }
574              
575             sub list :Via(init) At($name) {
576             my ($self, $c) = @_;
577             }
578              
579             __PACKAGE__->meta->make_immutable;
580              
581             package MyApp::Controller::ThingsTodo::Item;
582              
583             use Moose;
584             use MooseX::MethodAttributes;
585              
586             extends 'Catalyst::Controller';
587             with 'Catalyst::ControllerRole::At';
588              
589             sub init :Via($parent) At({id:Int}/...) {
590             my ($self, $c) = @_;
591             }
592              
593             sub show :Via(init) At($name) { ... }
594             sub update :Via(init) At($name) { ... }
595             sub delete :Via(init) At($name) { ... }
596              
597             __PACKAGE__->meta->make_immutable;
598              
599             This creates four (4) URL templates:
600              
601             https://localhost/thingstodo/list
602             https://localhost/thingstodo/:id/show
603             https://localhost/thingstodo/:id/update
604             https://localhost/thingstodo/:id/delete
605              
606             With an action execution flow as follows:
607              
608             https://localhost/thingstodo/list =>
609             /thingstodo/init
610             /thingstodo/list
611              
612             https://localhost/thingstodo/:id/show
613             /thingstodo/init
614             /thingstodo/item/init
615             /thingstodo/item/show
616              
617             https://localhost/thingstodo/:id/update
618             /thingstodo/init
619             /thingstodo/item/init
620             /thingstodo/item/update
621              
622             https://localhost/thingstodo/:id/delete
623             /thingstodo/init
624             /thingstodo/item/init
625             /thingstodo/item/delete
626              
627             =head1 COOKBOOK
628              
629             One thing I like to do is create a base controller for my project
630             so that I can make my controllers more concise:
631              
632             package Myapp::Controller;
633              
634             use Moose;
635             extends 'Catalyst::Controller';
636             with 'Catalyst::ControllerRole::At';
637            
638             __PACKAGE__->meta->make_immutable;
639              
640             You can of course doa lot more here if you want but I usually recommend
641             the lightest touch possible in your base controllers since the more you customize
642             the harder it might be for people new to the code to debug the system.
643              
644             =head1 TODO
645              
646             - HTTP Methods
647             - Incoming Content type matching
648             - ??Content Negotiation??
649              
650             =head1 AUTHOR
651            
652             John Napiorkowski L<email:jjnapiork@cpan.org>
653            
654             =head1 SEE ALSO
655            
656             L<Catalyst>, L<Catalyst::Controller>.
657            
658             =head1 COPYRIGHT & LICENSE
659            
660             Copyright 2016, John Napiorkowski L<email:jjnapiork@cpan.org>
661            
662             This library is free software; you can redistribute it and/or modify it under
663             the same terms as Perl itself.
664              
665             =cut