implemented action dispatching by type
[catagits/CatalystX-Declare.git] / lib / CatalystX / Declare / Keyword / Action.pm
1 use MooseX::Declare;
2 use MooseX::Role::Parameterized ();
3
4 class CatalystX::Declare::Keyword::Action
5     with MooseX::Declare::Syntax::KeywordHandling {
6
7
8     use Carp                qw( croak );
9     use Perl6::Junction     qw( any );
10     use Data::Dump          qw( pp );
11     use MooseX::Types::Util qw( has_available_type_export );
12     use Moose::Util         qw( add_method_modifier ensure_all_roles );
13     use Class::Inspector;
14     use Class::MOP;
15
16
17     use constant STOP_PARSING   => '__MXDECLARE_STOP_PARSING__';
18     use constant UNDER_VAR      => '$CatalystX::Declare::SCOPE::UNDER';
19     use constant UNDER_STACK    => '@CatalystX::Declare::SCOPE::UNDER_STACK';
20
21     use aliased 'CatalystX::Declare::Action::CatchValidationError';
22     use aliased 'MooseX::Method::Signatures::Meta::Method';
23     use aliased 'MooseX::MethodAttributes::Role::Meta::Method', 'AttributeRole';
24     use aliased 'MooseX::MethodAttributes::Role::Meta::Role',   'AttributeMetaRole';
25
26
27     method parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0) {
28
29         # somewhere to put the attributes
30         my %attributes;
31         my @populators;
32
33         # parse declarations
34         until (do { $ctx->skipspace; $ctx->peek_next_char } eq any qw( ; { } )) {
35
36             $ctx->skipspace;
37             
38             # optional commas
39             if ($ctx->peek_next_char eq ',') {
40
41                 my $linestr = $ctx->get_linestr;
42                 substr($linestr, $ctx->offset, 1) = '';
43                 $ctx->set_linestr($linestr);
44
45                 next;
46             }
47
48             # next thing should be an option name
49             my $option = (
50                 $skipped_declarator 
51                 ? $ctx->strip_name 
52                 : do { 
53                     $ctx->skip_declarator; 
54                     $skipped_declarator++;
55                     $ctx->declarator;
56                 })
57               or croak "Expected option token, not " . substr($ctx->get_linestr, $ctx->offset);
58
59             # we need to be able to handle the rest
60             my $handler = $self->can("_handle_${option}_option")
61                 or croak "Unknown action option: $option";
62
63             # call the handler
64             my $populator = $self->$handler($ctx, \%attributes);
65
66             if ($populator and $populator eq STOP_PARSING) {
67
68                 return $ctx->shadow(sub (&) {
69                     my ($body) = @_;
70                     return $body->();
71                 });
72             }
73
74             push @populators, $populator
75                 if defined $populator;
76         }
77
78         croak "Need an action specification"
79             unless exists $attributes{Signature};
80
81         my $name   = $attributes{Subname};
82
83         my $method = Method->wrap(
84             signature       => qq{($attributes{Signature})},
85             package_name    => $ctx->get_curstash_name,
86             name            => $name,
87         );
88
89         AttributeRole->meta->apply($method);
90
91         my $count = $self->_count_positional_arguments($method);
92         $attributes{CaptureArgs} = $count
93             if defined $count;
94
95         $_->($method)
96             for @populators;
97
98         unless ($attributes{Private}) {
99             $attributes{PathPart} ||= "'$name'";
100
101             delete $attributes{CaptureArgs}
102                 if exists $attributes{Args};
103
104             $attributes{CaptureArgs} = 0
105                 unless exists $attributes{Args}
106                     or exists $attributes{CaptureArgs};
107         }
108
109         if ($attributes{Private}) {
110             delete $attributes{ $_ }
111                 for qw( Args CaptureArgs Chained Signature Private );
112         }
113
114         if ($ctx->peek_next_char eq '{') {
115             $ctx->inject_if_block($ctx->scope_injector_call . $method->injectable_code);
116         }
117         else {
118             $ctx->inject_code_parts_here(
119                 sprintf '{ %s%s }',
120                     $ctx->scope_injector_call,
121                     $method->injectable_code,
122             );
123         }
124
125         my @attributes;
126         for my $attr (keys %attributes) {
127             push @attributes, 
128                 map { sprintf '%s%s', $attr, defined($_) ? sprintf('(%s)', $_) : '' }
129                     (ref($attributes{ $attr }) eq 'ARRAY') 
130                     ? @{ $attributes{ $attr } }
131                     : $attributes{ $attr };
132         }
133
134         return $ctx->shadow(sub (&) {
135             my $class = caller;
136             my $body  = shift;
137
138             $method->_set_actual_body($body);
139             $method->{attributes} = \@attributes;
140
141             if ($modifier) {
142
143                 add_method_modifier $class, $modifier, [$name, $method];
144             }
145             else {
146
147                 my $prepare_meta = sub {
148                     my ($meta) = @_;
149
150                     $meta->add_method($name, $method);
151                     $meta->register_method_attributes($meta->name->can($method->name), \@attributes);
152                 };
153
154                 if ($ctx->stack->[-1] and $ctx->stack->[-1]->is_parameterized) {
155                     my $real_meta = MooseX::Role::Parameterized->current_metaclass;
156
157                     $real_meta->meta->make_mutable
158                         if $real_meta->meta->is_immutable;
159                     ensure_all_roles $real_meta->meta, AttributeMetaRole
160                         if $real_meta->isa('Moose::Meta::Role');
161
162                     $real_meta->$prepare_meta;
163                 }
164
165                 $class->meta->$prepare_meta;
166             }
167         });
168     }
169
170     method _handle_with_option (Object $ctx, HashRef $attrs) {
171
172         my $role = $ctx->strip_name
173             or croak "Expected bareword role specification for action after with";
174
175         # we need to fish for aliases here since we are still unclean
176         if (defined(my $alias = $self->_check_for_available_import($ctx, $role))) {
177             $role = $alias;
178         }
179
180         push @{ $attrs->{CatalystX_Declarative_ActionRoles} ||= [] }, $role;
181
182         return;
183     }
184
185     method _handle_isa_option (Object $ctx, HashRef $attrs) {
186
187         my $class = $ctx->strip_name
188             or croak "Expected bareword action class specification for action after isa";
189
190         if (defined(my $alias = $self->_check_for_available_import($ctx, $class))) {
191             $class = $alias;
192         }
193
194         $attrs->{CatalystX_Declarative_ActionClass} = $class;
195
196         return;
197     }
198
199     method _check_for_available_import (Object $ctx, Str $name) {
200
201         if (my $code = $ctx->get_curstash_name->can($name)) {
202             return $code->();
203         }
204
205         return undef;
206     }
207
208     method _handle_action_option (Object $ctx, HashRef $attrs) {
209
210         # action name
211         my $name = $ctx->strip_name
212             or croak "Anonymous actions not yet supported";
213
214         $ctx->skipspace;
215         my $populator;
216
217         if (substr($ctx->get_linestr, $ctx->offset, 2) eq '<-') {
218             my $linestr = $ctx->get_linestr;
219             substr($linestr, $ctx->offset, 2) = '';
220             $ctx->set_linestr($linestr);
221             $populator = $self->_handle_under_option($ctx, $attrs);
222         }
223
224         # signature
225         my $proto = $ctx->strip_proto || '';
226         $proto = join(', ', 'Object $self: Object $ctx', $proto || ());
227
228         $attrs->{Subname}   = $name;
229         $attrs->{Signature} = $proto;
230         $attrs->{Action}    = [];
231
232         push @{ $attrs->{CatalystX_Declarative_ActionRoles} ||= [] }, CatchValidationError;
233
234         if (defined $CatalystX::Declare::SCOPE::UNDER) {
235             $attrs->{Chained} ||= $CatalystX::Declare::SCOPE::UNDER;
236         }
237
238         return unless $populator;
239         return $populator;
240     }
241
242     method _handle_final_option (Object $ctx, HashRef $attrs) {
243
244         return $self->_build_flag_populator($ctx, $attrs, 'final');
245     }
246
247     method _handle_is_option (Object $ctx, HashRef $attrs) {
248
249         my $what = $ctx->strip_name
250             or croak "Expected symbol token after is symbol, not " . substr($ctx->get_linestr, $ctx->offset);
251
252         return $self->_build_flag_populator($ctx, $attrs, $what);
253     }
254
255     method _build_flag_populator (Object $ctx, HashRef $attrs, Str $what) {
256
257         return sub {
258             my $method = shift;
259
260             if ($what eq any qw( end endpoint final )) {
261                 $attrs->{Args} = delete $attrs->{CaptureArgs};
262             }
263             elsif ($what eq 'private') {
264                 $attrs->{Private} = [];
265             }
266         };
267     }
268
269     method _handle_under_option (Object $ctx, HashRef $attrs) {
270
271         my $target = $self->_strip_actionpath($ctx);
272         $ctx->skipspace;
273
274         if ($ctx->peek_next_char eq '{' and $self->identifier eq 'under') {
275             $ctx->inject_if_block(
276                 sprintf '%s; BEGIN { push %s, %s; %s = qq(%s) };',
277                     $ctx->scope_injector_call(
278                         sprintf ';BEGIN { %s = pop %s };', 
279                             UNDER_VAR,
280                             UNDER_STACK,
281                     ),
282                     UNDER_STACK,
283                     UNDER_VAR,
284                     UNDER_VAR,
285                     $target,
286             );
287             return STOP_PARSING;
288         }
289
290         $attrs->{Chained} = "'$target'";
291
292         return sub {
293             my $method = shift;
294         };
295     }
296
297     method _handle_chains_option (Object $ctx, HashRef $attrs) {
298
299         $ctx->skipspace;
300         $ctx->strip_name eq 'to'
301             or croak "Expected to token after chains symbol, not " . substr($ctx->get_linestr, $ctx->offset);
302
303         return $self->_handle_under_option($ctx, $attrs);
304     }
305
306     method _handle_as_option (Object $ctx, HashRef $attrs) {
307
308         $ctx->skipspace;
309
310         my $path = $self->_strip_actionpath($ctx);
311         $attrs->{PathPart} = "'$path'";
312
313         return;
314     }
315
316     method _count_positional_arguments (Object $method) {
317         my $signature = $method->parsed_signature;
318
319         if ($signature->has_positional_params) {
320             my $count = @{ scalar($signature->positional_params) };
321
322             if ($count and ($signature->positional_params)[-1]->sigil eq '@') {
323                 return undef;
324             }
325
326             return $count - 1;
327         }
328
329         return 0;
330     }
331
332     method _strip_actionpath (Object $ctx) {
333
334         $ctx->skipspace;
335         my $linestr = $ctx->get_linestr;
336         my $rest    = substr($linestr, $ctx->offset);
337
338         if ($rest =~ /^ ( [_a-z] [_a-z0-9]* ) \b/ix) {
339             substr($linestr, $ctx->offset, length($1)) = '';
340             $ctx->set_linestr($linestr);
341             return $1;
342         }
343         elsif ($rest =~ /^ ' ( (?:[.:;,_a-z0-9]|\/)* ) ' /ix) {
344             substr($linestr, $ctx->offset, length($1) + 2) = '';
345             $ctx->set_linestr($linestr);
346             return $1;
347         }
348         else {
349             croak "Invalid syntax for action path: $rest";
350         }
351     }
352 }
353
354 __END__
355
356 =head1 NAME
357
358 CatalystX::Declare::Keyword::Action - Declare Catalyst Actions
359
360 =head1 SYNOPSIS
361
362     use CatalystX::Declare;
363
364     controller MyApp::Web::Controller::Example {
365
366         # chain base action with path part setting of ''
367         # body-less actions don't do anything by themselves
368         action base as '' under '/';
369
370         # simple end-point action
371         action controller_class is final under base {
372             $ctx->response->body( 'controller: ' . ref $self );
373         }
374
375         # chain part actions can have arguments
376         action str (Str $string) under base {
377
378             $ctx->stash(chars => [split //, $string]);
379         }
380
381         # and end point actions too, of course
382         action uc_chars (Int $count) under str is final {
383     
384             my $chars = $ctx->stash->{chars};
385             ...
386         }
387
388
389         # you can use a shortcut for multiple actions with
390         # a common base
391         under base {
392
393             # this is an endpoint after base
394             action normal is final;
395
396             # the final keyword can be used to be more 
397             # visually explicit about end-points
398             final action some_action { ... }
399
400             # type dispatching works
401             final action with_str (Str $x) as via_type;
402             final action with_int (Int $x) as via_type;
403         }
404
405         # of course you can also chain to external actions
406         final action some_end under '/some/controller/some/action';
407     }
408
409 =head1 DESCRIPTION
410
411 This handler class provides the user with C<action>, C<final> and C<under> 
412 keywords. There are multiple ways to define actions to allow for greater
413 freedom of expression. While the parts of the action declaration itself do
414 not care about their order, their syntax is rather strict.
415
416 You can choose to separate syntax elements via C<,> if you think it is more
417 readable. The action declaration
418
419     action foo is final under base;
420
421 is parsed in exactly the same way if you write it as
422
423     action foo, is final, under base;
424
425 =head2 Basic Action Declaration
426
427 The simplest possible declaration is
428
429     action foo;
430
431 This would define a chain-part action chained to nothing with the name C<foo>
432 and no arguments. Since it isn't followed by a block, the body of the action
433 will be empty.
434
435 You will automatically be provided with two variables: C<$self> is, as you
436 might expect, your controller instance. C<$ctx> will be the Catalyst context
437 object. Thus, the following code would stash the value returned by the 
438 C<get_item> method:
439
440     action foo {
441         $ctx->stash(item => $self->get_item);
442     }
443
444 =head2 Setting a Path Part
445
446 As usual with Catalyst actions, the path part (the public name of this part of
447 the URI, if you're not familiar with the term yet) will default to the name of
448 the action itself (or more correctly: to whatever Catalyst defaults).
449
450 To change that, use the C<as> option:
451
452     under something {
453         action base      as '';             # <empty>
454         action something as 'foo/bar';      # foo/bar
455         action barely    as bareword;       # bareword
456     }
457
458 =head2 Chaining Actions
459
460 Currently, L<CatalystX::Declare> is completely based on the concept of
461 L<chained actions|Catalyst::DispatchType::Chained>. Every action you declare is
462 chained or private. You can specify the action you want to chain to with the 
463 C<under> option:
464
465     action foo;                     # chained to nothing
466     action foo under '/';           # also chained to /
467     action foo under bar;           # chained to the local bar action
468     action foo under '/bar/baz';    # chained to baz in /bar
469
470 C<under> is also provided as a grouping keyword. Every action inside the block
471 will be chained to the specified action:
472
473     under base {
474         action foo { ... }
475         action bar { ... }
476     }
477
478 You can also use the C<under> keyword for a single action. This is useful if
479 you want to highlight a single action with a significant diversion from what
480 is to be expected:
481
482     action base under '/';
483
484     under '/the/sink' is final action foo;
485
486     final action bar under base;
487
488     final action baz under base;
489
490 Instead of the C<under> option declaration, you can also use a more english
491 variant named C<chains to>. While C<under> might be nice and concise, some
492 people might prefer this if they confuse C<under> with the specification of
493 a public path part. The argument to C<chains to> is the same as to C<under>:
494
495     action foo chains to bar;
496     action foo under bar;
497
498 By default all actions are chain-parts, not end-points. If you want an action 
499 to be picked up as end-point and available via a public path, you have to say
500 so explicitely by  using the C<is final> option:
501
502     action base under '/';
503     action foo under base is final;   # /base/foo
504
505 You can also drop the C<is> part of the C<is final> option if you want:
506
507     under base, final action foo { ... }
508
509 You can make end-points more visually distinct by using the C<final> keyword
510 instead of the option:
511
512     action base under '/';
513     final action foo under base;      # /base/foo
514
515 And of course, the C<final>, C<under> and C<action> keywords can be used in
516 combination whenever needed:
517
518     action base as '' under '/';
519
520     under base {
521
522         final action list;          # /list
523
524         action load;
525
526         under load {
527
528             final action view;      # /list/load/view
529             final action edit;      # /list/load/edit
530         }
531     }
532
533 There is also one shorthand alternative for declaring chain targets. You can
534 specify an action after a C<E<lt>-> following the action name:
535
536     action base under '/';
537     final action foo <- base;       # /base/foo
538
539 =head2 Arguments
540
541 You can use signatures like you are use to from L<MooseX::Method::Signatures>
542 to declare action parameters. The number of positinoal arguments will be used 
543 during dispatching as well as their types.
544
545 The signature follows the action name:
546
547     # /foo/*/*/*
548     final action foo (Int $year, Int $month, Int $day);
549
550 If you are using the shorthand definition, the signature follows the chain 
551 target:
552
553     # /foo/*
554     final action foo <- base ($x) under '/' { ... }
555
556 Parameters may be specified on chain-parts and end-points:
557
558     # /base/*/foo/*
559     action base (Str $lang) under '/';
560     final action page (Int $page_num) under base;
561
562 Named parameters will be populated with the values in the query parameters:
563
564     # /view/17/?page=3
565     final action view (Int $id, Int :$page = 1) under '/';
566
567 Your end-points can also take an unspecified amount of arguments by specifying
568 an array as a variable:
569
570     # /find/some/deep/path/spec
571     final action find (@path) under '/';
572
573 =head2 Validation
574
575 The signatures are now validated during dispatching-time, and an action with
576 a non-matching signature (number of positional arguments and their types) will
577 not be dispatched to. This means that
578
579     action base under '/' as '';
580
581     under base {
582
583         final as double, action double_string (Str $x) {
584             $ctx->response->body( $x x 2 );
585         }
586
587         final as double, action double_integer (Int $x) {
588             $ctx->response->body( $x * 2 );
589         }
590     }
591
592 will return C<foofoo> when called as C</double/foo> and C<46> when called as
593 C</double/23>.
594
595 =head2 Actions and Method Modifiers
596
597 Method modifiers can not only be applied to methods, but also to actions. There
598 is no way yet to override the attributes of an already established action via
599 modifiers. However, you can modify the method underlying the action.
600
601 The following code is an example role modifying the consuming controller's
602 C<base> action:
603
604     use CatalystX::Declare;
605
606     controller_role MyApp::Web::ControllerRole::RichBase {
607
608         before base (Object $ctx) {
609             $ctx->stash(something => $ctx->model('Item'));
610         }
611     }
612
613 Note that you have to specify the C<$ctx> argument yourself, since you are 
614 modifying a method, not an action.
615
616 Any controller having a C<base> action (or method, for this purpose), can now
617 consume the C<RichBase> role declared above:
618
619     use CatalystX::Declare;
620
621     controller MyApp::Web::Controller::Foo
622         with   MyApp::Web::Controller::RichBase {
623
624         action base as '' under '/';
625
626         action show, final under base { 
627             $ctx->response->body(
628                 $ctx->stash->{something}->render,
629             );
630         }
631     }
632
633 =head2 Action Classes
634
635 B<This option is even more experimental>
636
637 You might want to create an action with a different class than the usual
638 L<Catalyst::Action>. A usual suspect here is L<Catalyst::Action::RenderView>.
639 You can use the C<isa> option (did I mention it's experimental?) to specify
640 what class to use:
641
642     controller MyApp::Web::Controller::Root {
643
644         $CLASS->config(namespace => '');
645
646         action end isa RenderView;
647     }
648
649 The loaded class will be L<Moose>ified, so we are able to apply essential
650 roles.
651
652 =head1 ROLES
653
654 =over
655
656 =item L<MooseX::Declare::Syntax::KeywordHandling>
657
658 =back
659
660 =head1 METHODS
661
662 These methods are implementation details. Unless you are extending or 
663 developing L<CatalystX::Declare>, you should not be concerned with them.
664
665 =head2 parse
666
667     Object->parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0)
668
669 A hook that will be invoked by L<MooseX::Declare> when this instance is called 
670 to handle syntax. It will parse the action declaration, prepare attributes and 
671 add the actions to the controller.
672
673 =head1 SEE ALSO
674
675 =over
676
677 =item L<CatalystX::Declare>
678
679 =item L<CatalystX::Declare::Keyword::Controller>
680
681 =item L<MooseX::Method::Signatures>
682
683 =back
684
685 =head1 AUTHOR
686
687 See L<CatalystX::Declare/AUTHOR> for author information.
688
689 =head1 LICENSE
690
691 This program is free software; you can redistribute it and/or modify it under 
692 the same terms as perl itself.
693
694 =cut
695