2 use MooseX::Role::Parameterized ();
4 class CatalystX::Declare::Keyword::Action {
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 );
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';
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';
27 method parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0) {
29 # somewhere to put the attributes
34 until (do { $ctx->skipspace; $ctx->peek_next_char } eq any qw( ; { } )) {
39 if ($ctx->peek_next_char eq ',') {
41 my $linestr = $ctx->get_linestr;
42 substr($linestr, $ctx->offset, 1) = '';
43 $ctx->set_linestr($linestr);
48 # next thing should be an option name
53 $ctx->skip_declarator;
54 $skipped_declarator++;
57 or croak "Expected option token, not " . substr($ctx->get_linestr, $ctx->offset);
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";
64 my $populator = $self->$handler($ctx, \%attributes);
66 if ($populator and $populator eq STOP_PARSING) {
68 return $ctx->shadow(sub (&) {
74 push @populators, $populator
75 if defined $populator;
78 croak "Need an action specification"
79 unless exists $attributes{Signature};
81 my $name = $attributes{Subname};
83 if ($attributes{Private}) {
84 $attributes{Signature} ||= '@';
87 my $method = Method->wrap(
88 signature => qq{($attributes{Signature})},
89 package_name => $ctx->get_curstash_name,
93 AttributeRole->meta->apply($method);
95 my $count = $self->_count_positional_arguments($method);
96 $attributes{CaptureArgs} = $count
102 unless ($attributes{Private}) {
103 $attributes{PathPart} ||= $name;
105 delete $attributes{CaptureArgs}
106 if exists $attributes{Args};
108 $attributes{CaptureArgs} = 0
109 unless exists $attributes{Args}
110 or exists $attributes{CaptureArgs};
113 if ($attributes{Private}) {
114 delete $attributes{ $_ }
115 for qw( Args CaptureArgs Chained Signature Private );
118 # inject a hashref for resolving runtime attribute values
119 $self->_inject_attributes($ctx, \%attributes);
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);
126 # there is no block, so we insert one.
128 $ctx->inject_code_parts_here(
130 $ctx->scope_injector_call,
131 $method->injectable_code,
135 my $compile_attrs = sub {
136 my $attributes = shift;;
139 for my $attr (keys %$attributes) {
140 my $value = $attributes->{ $attr };
142 # the compiletime chained attr might contain the under global var name
143 next if $attr eq 'Chained' and $value eq UNDER_VAR;
146 map { sprintf '%s%s', $attr, defined($_) ? sprintf('(%s)', $_) : '' }
147 (ref($value) eq 'ARRAY')
155 return $ctx->shadow(sub {
160 # the runtime-resolved name
161 my $name = $attrs->{Subname};
163 # in case no hashref was specified
164 $body = $attrs and $attrs = {}
165 if ref $attrs eq 'CODE';
167 # default path part to runtime-resolved name
168 unless ($attrs->{Private}) {
170 $attrs->{PathPart} = $attrs->{Subname}
171 unless defined $attrs->{PathPart};
174 # in CXD we are explicit about chained values, an undefined
175 # value means we defaulted to the outer-scope under and there
177 delete $attrs->{Chained}
178 unless defined $attrs->{Chained};
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 );
184 # merge runtime and compiletime attrs
185 my %full_attrs = (%attributes, %$attrs);
186 my $compiled_attrs = $compile_attrs->(\%full_attrs);
188 my $real_method = $method->reify(
189 actual_body => $body,
190 attributes => $compiled_attrs,
197 add_method_modifier $class, $modifier, [$name, $real_method];
201 my $prepare_meta = sub {
204 $meta->add_method($name, $real_method);
205 $meta->register_method_attributes($meta->name->can($real_method->name), $compiled_attrs);
208 if ($ctx->stack->[-1] and $ctx->stack->[-1]->is_parameterized) {
209 my $real_meta = MooseX::Role::Parameterized->current_metaclass;
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');
216 $real_meta->$prepare_meta;
220 $class->meta->$prepare_meta;
226 method _handle_with_option (Object $ctx, HashRef $attrs) {
228 my @roles_with_args = ();
229 push @roles_with_args, @{ $ctx->strip_names_and_args };
231 # we need to fish for aliases here since we are still unclean
233 for my $role_with_arg(@roles_with_args) {
234 my $role = $role_with_arg->[0];
235 if (defined(my $alias = $self->_check_for_available_import($ctx, $role))) {
241 push @{ $attrs->{CatalystX_Declarative_ActionRoles} ||= [] }, @roles;
246 method _handle_isa_option (Object $ctx, HashRef $attrs) {
248 my $class = $ctx->strip_name
249 or croak "Expected bareword action class specification for action after isa";
251 if (defined(my $alias = $self->_check_for_available_import($ctx, $class))) {
255 $attrs->{CatalystX_Declarative_ActionClass} = $class;
260 method _check_for_available_import (Object $ctx, Str $name) {
262 if (my $code = $ctx->get_curstash_name->can($name)) {
269 method _handle_action_option (Object $ctx, HashRef $attrs) {
272 my $name = $self->_strip_actionpath($ctx, interpolate => 1)
273 or croak "Anonymous actions not yet supported";
278 # shortcut under base option is basically handled by the under handler
279 if (substr($ctx->get_linestr, $ctx->offset, 2) eq '<-') {
280 my $linestr = $ctx->get_linestr;
281 substr($linestr, $ctx->offset, 2) = '';
282 $ctx->set_linestr($linestr);
283 $populator = $self->_handle_under_option($ctx, $attrs);
287 my $proto = $ctx->strip_proto || '';
288 $proto = join(', ', 'Object $self: Object $ctx', $proto || ());
290 $attrs->{Subname} = $name;
291 $attrs->{Signature} = $proto;
292 $attrs->{Action} = [];
294 push @{ $attrs->{CatalystX_Declarative_ActionRoles} ||= [] }, CatchValidationError;
296 # default chained base to the global under var, to be resolved at runtime
297 $attrs->{Chained} ||= UNDER_VAR;
299 return unless $populator;
303 method _handle_final_option (Object $ctx, HashRef $attrs) {
305 return $self->_build_flag_populator($ctx, $attrs, 'final');
308 method _handle_is_option (Object $ctx, HashRef $attrs) {
310 my $what = $ctx->strip_name
311 or croak "Expected symbol token after is symbol, not " . substr($ctx->get_linestr, $ctx->offset);
313 return $self->_build_flag_populator($ctx, $attrs, $what);
316 method _build_flag_populator (Object $ctx, HashRef $attrs, Str $what) {
318 $attrs->{Private} = []
319 if $what eq 'private';
324 if ($what eq any qw( end endpoint final )) {
325 $attrs->{Args} = delete $attrs->{CaptureArgs};
327 elsif ($what eq 'private') {
328 $attrs->{Private} = [];
333 method _handle_under_option (Object $ctx, HashRef $attrs) {
335 my $target = $self->_strip_actionpath($ctx, interpolate => 1);
338 if ($ctx->peek_next_char eq '{' and $self->identifier eq 'under') {
339 $ctx->inject_if_block(
340 $ctx->scope_injector_call .
341 sprintf ';local %s = %s;',
348 $attrs->{Chained} = $target;
355 method _handle_chains_option (Object $ctx, HashRef $attrs) {
358 $ctx->strip_name eq 'to'
359 or croak "Expected to token after chains symbol, not " . substr($ctx->get_linestr, $ctx->offset);
361 return $self->_handle_under_option($ctx, $attrs);
364 method _handle_as_option (Object $ctx, HashRef $attrs) {
368 my $path = $self->_strip_actionpath($ctx, interpolate => 1);
369 $attrs->{PathPart} = $path;
374 method _count_positional_arguments (Object $method) {
375 my $signature = $method->parsed_signature;
377 if ($signature->has_positional_params) {
378 my $count = @{ scalar($signature->positional_params) };
380 if ($count and ($signature->positional_params)[-1]->sigil eq '@') {
390 method _inject_attributes (Object $ctx, HashRef $attrs) {
392 # attrs that need to be runtime-resolved
393 my @inject = qw( Chained PathPart Subname );
395 # turn specific attributes into a hashref
396 my $code = sprintf ' +{ %s }, sub ', # the ', sub ' turns method +{ ... } { ... } into
397 join ', ', # method +{ ... }, sub { ... }
399 map { defined( $_->[1] ) ? $_ : [$_->[0], 'undef'] }
400 map { [pp($_), $attrs->{ $_ }] }
401 grep { defined $attrs->{ $_ } }
404 # inject the hashref code before the action body
405 $ctx->inject_code_parts_here($code);
406 $ctx->inc_offset(length $code);
409 method _strip_actionpath (Object $ctx, :$interpolate?) {
412 my $linestr = $ctx->get_linestr;
413 my $rest = substr($linestr, $ctx->offset);
414 my $interp = sub { $interpolate ? "'$_[0]'" : $_[0] };
416 # find simple barewords
417 if ($rest =~ /^ ( [_a-z] [_a-z0-9]* ) \b/ix) {
418 substr($linestr, $ctx->offset, length($1)) = '';
419 $ctx->set_linestr($linestr);
420 return $interp->($1);
423 # allow single quoted more complex barewords
424 elsif ($rest =~ /^ ' ( (?:[.:;,_a-z0-9]|\/)* ) ' /ix) {
425 substr($linestr, $ctx->offset, length($1) + 2) = '';
426 $ctx->set_linestr($linestr);
427 return $interp->($1);
430 # double quoted strings and variables
431 elsif ($interpolate and my $str = $ctx->get_string) {
435 # not suitable as action path
437 croak "Invalid syntax for action path: $rest";
441 # down here because it requires the parse method
442 with 'MooseX::Declare::Syntax::KeywordHandling';
444 around context_traits { $self->$orig, StringParsing }
451 CatalystX::Declare::Keyword::Action - Declare Catalyst Actions
455 use CatalystX::Declare;
457 controller MyApp::Web::Controller::Example {
459 # chain base action with path part setting of ''
460 # body-less actions don't do anything by themselves
461 action base as '' under '/';
463 # simple end-point action
464 action controller_class is final under base {
465 $ctx->response->body( 'controller: ' . ref $self );
468 # chain part actions can have arguments
469 action str (Str $string) under base {
471 $ctx->stash(chars => [split //, $string]);
474 # and end point actions too, of course
475 action uc_chars (Int $count) under str is final {
477 my $chars = $ctx->stash->{chars};
482 # you can use a shortcut for multiple actions with
486 # this is an endpoint after base
487 action normal is final;
489 # the final keyword can be used to be more
490 # visually explicit about end-points
491 final action some_action { ... }
493 # type dispatching works
494 final action with_str (Str $x) as via_type;
495 final action with_int (Int $x) as via_type;
498 # of course you can also chain to external actions
499 final action some_end under '/some/controller/some/action';
504 This handler class provides the user with C<action>, C<final> and C<under>
505 keywords. There are multiple ways to define actions to allow for greater
506 freedom of expression. While the parts of the action declaration itself do
507 not care about their order, their syntax is rather strict.
509 You can choose to separate syntax elements via C<,> if you think it is more
510 readable. The action declaration
512 action foo is final under base;
514 is parsed in exactly the same way if you write it as
516 action foo, is final, under base;
518 =head2 Basic Action Declaration
520 The simplest possible declaration is
524 This would define a chain-part action chained to nothing with the name C<foo>
525 and no arguments. Since it isn't followed by a block, the body of the action
528 You will automatically be provided with two variables: C<$self> is, as you
529 might expect, your controller instance. C<$ctx> will be the Catalyst context
530 object. Thus, the following code would stash the value returned by the
534 $ctx->stash(item => $self->get_item);
537 =head2 Why $ctx instead of $c
539 Some might ask why the context object is called C<$ctx> instead of the usual
540 C<$c>. The reason is simple: It's an opinionated best practice, since C<$ctx>
543 =head2 Setting a Path Part
545 As usual with Catalyst actions, the path part (the public name of this part of
546 the URI, if you're not familiar with the term yet) will default to the name of
547 the action itself (or more correctly: to whatever Catalyst defaults).
549 To change that, use the C<as> option:
552 action base as ''; # <empty>
553 action something as 'foo/bar'; # foo/bar
554 action barely as bareword; # bareword
557 =head2 Chaining Actions
559 Currently, L<CatalystX::Declare> is completely based on the concept of
560 L<chained actions|Catalyst::DispatchType::Chained>. Every action you declare is
561 chained or private. You can specify the action you want to chain to with the
564 action foo; # chained to nothing
565 action foo under '/'; # also chained to /
566 action foo under bar; # chained to the local bar action
567 action foo under '/bar/baz'; # chained to baz in /bar
569 C<under> is also provided as a grouping keyword. Every action inside the block
570 will be chained to the specified action:
577 You can also use the C<under> keyword for a single action. This is useful if
578 you want to highlight a single action with a significant diversion from what
581 action base under '/';
583 under '/the/sink' is final action foo;
585 final action bar under base;
587 final action baz under base;
589 Instead of the C<under> option declaration, you can also use a more english
590 variant named C<chains to>. While C<under> might be nice and concise, some
591 people might prefer this if they confuse C<under> with the specification of
592 a public path part. The argument to C<chains to> is the same as to C<under>:
594 action foo chains to bar;
595 action foo under bar;
597 By default all actions are chain-parts, not end-points. If you want an action
598 to be picked up as end-point and available via a public path, you have to say
599 so explicitely by using the C<is final> option:
601 action base under '/';
602 action foo under base is final; # /base/foo
604 You can also drop the C<is> part of the C<is final> option if you want:
606 under base, final action foo { ... }
608 You can make end-points more visually distinct by using the C<final> keyword
609 instead of the option:
611 action base under '/';
612 final action foo under base; # /base/foo
614 And of course, the C<final>, C<under> and C<action> keywords can be used in
615 combination whenever needed:
617 action base as '' under '/';
621 final action list; # /list
627 final action view; # /list/load/view
628 final action edit; # /list/load/edit
632 There is also one shorthand alternative for declaring chain targets. You can
633 specify an action after a C<E<lt>-> following the action name:
635 action base under '/';
636 final action foo <- base; # /base/foo
640 You can use signatures like you are use to from L<MooseX::Method::Signatures>
641 to declare action parameters. The number of positinoal arguments will be used
642 during dispatching as well as their types.
644 The signature follows the action name:
647 final action foo (Int $year, Int $month, Int $day);
649 If you are using the shorthand definition, the signature follows the chain
653 final action foo <- base ($x) under '/' { ... }
655 Parameters may be specified on chain-parts and end-points:
658 action base (Str $lang) under '/';
659 final action page (Int $page_num) under base;
661 Named parameters will be populated with the values in the query parameters:
664 final action view (Int $id, Int :$page = 1) under '/';
666 If you specify a query parameter to be an C<ArrayRef>, it will be specially
667 handled. For one, it will match even if there is no such value in the
668 parameters. Second, it will always be wrapped as an array reference.
670 Your end-points can also take an unspecified amount of arguments by specifying
671 an array as a variable:
673 # /find/some/deep/path/spec
674 final action find (@path) under '/';
678 The signatures are now validated during dispatching-time, and an action with
679 a non-matching signature (number of positional arguments and their types) will
680 not be dispatched to. This means that
682 action base under '/' as '';
686 final as double, action double_integer (Int $x) {
687 $ctx->response->body( $x * 2 );
690 final as double, action double_string (Str $x) {
691 $ctx->response->body( $x x 2 );
695 will return C<foofoo> when called as C</double/foo> and C<46> when called as
698 =head2 Actions and Method Modifiers
700 Method modifiers can not only be applied to methods, but also to actions. There
701 is no way yet to override the attributes of an already established action via
702 modifiers. However, you can modify the method underlying the action.
704 The following code is an example role modifying the consuming controller's
707 use CatalystX::Declare;
709 controller_role MyApp::Web::ControllerRole::RichBase {
711 before base (Object $ctx) {
712 $ctx->stash(something => $ctx->model('Item'));
716 Note that you have to specify the C<$ctx> argument yourself, since you are
717 modifying a method, not an action.
719 Any controller having a C<base> action (or method, for this purpose), can now
720 consume the C<RichBase> role declared above:
722 use CatalystX::Declare;
724 controller MyApp::Web::Controller::Foo
725 with MyApp::Web::Controller::RichBase {
727 action base as '' under '/';
729 action show, final under base {
730 $ctx->response->body(
731 $ctx->stash->{something}->render,
736 =head2 Action Classes
738 B<This option is even more experimental>
740 You might want to create an action with a different class than the usual
741 L<Catalyst::Action>. A usual suspect here is L<Catalyst::Action::RenderView>.
742 You can use the C<isa> option (did I mention it's experimental?) to specify
745 controller MyApp::Web::Controller::Root {
747 $CLASS->config(namespace => '');
749 action end isa RenderView;
752 The loaded class will be L<Moose>ified, so we are able to apply essential
755 =head2 Private Actions
757 B<This option is a bit less, but still pretty experimental>
759 You can declare private actions with the C<is private> trait:
761 action end is private isa RenderView;
767 =item L<MooseX::Declare::Syntax::KeywordHandling>
773 These methods are implementation details. Unless you are extending or
774 developing L<CatalystX::Declare>, you should not be concerned with them.
778 Object->parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0)
780 A hook that will be invoked by L<MooseX::Declare> when this instance is called
781 to handle syntax. It will parse the action declaration, prepare attributes and
782 add the actions to the controller.
788 =item L<CatalystX::Declare>
790 =item L<CatalystX::Declare::Keyword::Controller>
792 =item L<MooseX::Method::Signatures>
798 See L<CatalystX::Declare/AUTHOR> for author information.
802 This program is free software; you can redistribute it and/or modify it under
803 the same terms as perl itself.