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