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