2 use MooseX::Role::Parameterized ();
4 class CatalystX::Declare::Keyword::Action
5 with MooseX::Declare::Syntax::KeywordHandling {
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 );
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';
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';
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 my $method = Method->wrap(
84 signature => qq{($attributes{Signature})},
85 package_name => $ctx->get_curstash_name,
89 AttributeRole->meta->apply($method);
91 my $count = $self->_count_positional_arguments($method);
92 $attributes{CaptureArgs} = $count
98 unless ($attributes{Private}) {
99 $attributes{PathPart} ||= "'$name'";
101 delete $attributes{CaptureArgs}
102 if exists $attributes{Args};
104 $attributes{CaptureArgs} = 0
105 unless exists $attributes{Args}
106 or exists $attributes{CaptureArgs};
109 if ($attributes{Private}) {
110 delete $attributes{ $_ }
111 for qw( Args CaptureArgs Chained Signature Private );
114 if ($ctx->peek_next_char eq '{') {
115 $ctx->inject_if_block($ctx->scope_injector_call . $method->injectable_code);
118 $ctx->inject_code_parts_here(
120 $ctx->scope_injector_call,
121 $method->injectable_code,
126 for my $attr (keys %attributes) {
128 map { sprintf '%s%s', $attr, defined($_) ? sprintf('(%s)', $_) : '' }
129 (ref($attributes{ $attr }) eq 'ARRAY')
130 ? @{ $attributes{ $attr } }
131 : $attributes{ $attr };
134 return $ctx->shadow(sub (&) {
138 $method->_set_actual_body($body);
139 $method->{attributes} = \@attributes;
143 add_method_modifier $class, $modifier, [$name, $method];
147 my $prepare_meta = sub {
150 $meta->add_method($name, $method);
151 $meta->register_method_attributes($meta->name->can($method->name), \@attributes);
154 if ($ctx->stack->[-1] and $ctx->stack->[-1]->is_parameterized) {
155 my $real_meta = MooseX::Role::Parameterized->current_metaclass;
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');
162 $real_meta->$prepare_meta;
165 $class->meta->$prepare_meta;
170 method _handle_with_option (Object $ctx, HashRef $attrs) {
172 my $role = $ctx->strip_name
173 or croak "Expected bareword role specification for action after with";
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))) {
180 push @{ $attrs->{CatalystX_Declarative_ActionRoles} ||= [] }, $role;
185 method _handle_isa_option (Object $ctx, HashRef $attrs) {
187 my $class = $ctx->strip_name
188 or croak "Expected bareword action class specification for action after isa";
190 if (defined(my $alias = $self->_check_for_available_import($ctx, $class))) {
194 $attrs->{CatalystX_Declarative_ActionClass} = $class;
199 method _check_for_available_import (Object $ctx, Str $name) {
201 if (my $code = $ctx->get_curstash_name->can($name)) {
208 method _handle_action_option (Object $ctx, HashRef $attrs) {
211 my $name = $ctx->strip_name
212 or croak "Anonymous actions not yet supported";
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);
225 my $proto = $ctx->strip_proto || '';
226 $proto = join(', ', 'Object $self: Object $ctx', $proto || ());
228 $attrs->{Subname} = $name;
229 $attrs->{Signature} = $proto;
230 $attrs->{Action} = [];
232 push @{ $attrs->{CatalystX_Declarative_ActionRoles} ||= [] }, CatchValidationError;
234 if (defined $CatalystX::Declare::SCOPE::UNDER) {
235 $attrs->{Chained} ||= $CatalystX::Declare::SCOPE::UNDER;
238 return unless $populator;
242 method _handle_final_option (Object $ctx, HashRef $attrs) {
244 return $self->_build_flag_populator($ctx, $attrs, 'final');
247 method _handle_is_option (Object $ctx, HashRef $attrs) {
249 my $what = $ctx->strip_name
250 or croak "Expected symbol token after is symbol, not " . substr($ctx->get_linestr, $ctx->offset);
252 return $self->_build_flag_populator($ctx, $attrs, $what);
255 method _build_flag_populator (Object $ctx, HashRef $attrs, Str $what) {
260 if ($what eq any qw( end endpoint final )) {
261 $attrs->{Args} = delete $attrs->{CaptureArgs};
263 elsif ($what eq 'private') {
264 $attrs->{Private} = [];
269 method _handle_under_option (Object $ctx, HashRef $attrs) {
271 my $target = $self->_strip_actionpath($ctx);
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 };',
290 $attrs->{Chained} = "'$target'";
297 method _handle_chains_option (Object $ctx, HashRef $attrs) {
300 $ctx->strip_name eq 'to'
301 or croak "Expected to token after chains symbol, not " . substr($ctx->get_linestr, $ctx->offset);
303 return $self->_handle_under_option($ctx, $attrs);
306 method _handle_as_option (Object $ctx, HashRef $attrs) {
310 my $path = $self->_strip_actionpath($ctx);
311 $attrs->{PathPart} = "'$path'";
316 method _count_positional_arguments (Object $method) {
317 my $signature = $method->parsed_signature;
319 if ($signature->has_positional_params) {
320 my $count = @{ scalar($signature->positional_params) };
322 if ($count and ($signature->positional_params)[-1]->sigil eq '@') {
332 method _strip_actionpath (Object $ctx) {
335 my $linestr = $ctx->get_linestr;
336 my $rest = substr($linestr, $ctx->offset);
338 if ($rest =~ /^ ( [_a-z] [_a-z0-9]* ) \b/ix) {
339 substr($linestr, $ctx->offset, length($1)) = '';
340 $ctx->set_linestr($linestr);
343 elsif ($rest =~ /^ ' ( (?:[.:;,_a-z0-9]|\/)* ) ' /ix) {
344 substr($linestr, $ctx->offset, length($1) + 2) = '';
345 $ctx->set_linestr($linestr);
349 croak "Invalid syntax for action path: $rest";
358 CatalystX::Declare::Keyword::Action - Declare Catalyst Actions
362 use CatalystX::Declare;
364 controller MyApp::Web::Controller::Example {
366 # chain base action with path part setting of ''
367 # body-less actions don't do anything by themselves
368 action base as '' under '/';
370 # simple end-point action
371 action controller_class is final under base {
372 $ctx->response->body( 'controller: ' . ref $self );
375 # chain part actions can have arguments
376 action str (Str $string) under base {
378 $ctx->stash(chars => [split //, $string]);
381 # and end point actions too, of course
382 action uc_chars (Int $count) under str is final {
384 my $chars = $ctx->stash->{chars};
389 # you can use a shortcut for multiple actions with
393 # this is an endpoint after base
394 action normal is final;
396 # the final keyword can be used to be more
397 # visually explicit about end-points
398 final action some_action { ... }
400 # type dispatching works
401 final action with_str (Str $x) as via_type;
402 final action with_int (Int $x) as via_type;
405 # of course you can also chain to external actions
406 final action some_end under '/some/controller/some/action';
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.
416 You can choose to separate syntax elements via C<,> if you think it is more
417 readable. The action declaration
419 action foo is final under base;
421 is parsed in exactly the same way if you write it as
423 action foo, is final, under base;
425 =head2 Basic Action Declaration
427 The simplest possible declaration is
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
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
441 $ctx->stash(item => $self->get_item);
444 =head2 Why $ctx instead of $c
446 Some might ask why the context object is called C<$ctx> instead of the usual
447 C<$c>. The reason is simple: It's an opinionated best practice, since C<$ctx>
450 =head2 Setting a Path Part
452 As usual with Catalyst actions, the path part (the public name of this part of
453 the URI, if you're not familiar with the term yet) will default to the name of
454 the action itself (or more correctly: to whatever Catalyst defaults).
456 To change that, use the C<as> option:
459 action base as ''; # <empty>
460 action something as 'foo/bar'; # foo/bar
461 action barely as bareword; # bareword
464 =head2 Chaining Actions
466 Currently, L<CatalystX::Declare> is completely based on the concept of
467 L<chained actions|Catalyst::DispatchType::Chained>. Every action you declare is
468 chained or private. You can specify the action you want to chain to with the
471 action foo; # chained to nothing
472 action foo under '/'; # also chained to /
473 action foo under bar; # chained to the local bar action
474 action foo under '/bar/baz'; # chained to baz in /bar
476 C<under> is also provided as a grouping keyword. Every action inside the block
477 will be chained to the specified action:
484 You can also use the C<under> keyword for a single action. This is useful if
485 you want to highlight a single action with a significant diversion from what
488 action base under '/';
490 under '/the/sink' is final action foo;
492 final action bar under base;
494 final action baz under base;
496 Instead of the C<under> option declaration, you can also use a more english
497 variant named C<chains to>. While C<under> might be nice and concise, some
498 people might prefer this if they confuse C<under> with the specification of
499 a public path part. The argument to C<chains to> is the same as to C<under>:
501 action foo chains to bar;
502 action foo under bar;
504 By default all actions are chain-parts, not end-points. If you want an action
505 to be picked up as end-point and available via a public path, you have to say
506 so explicitely by using the C<is final> option:
508 action base under '/';
509 action foo under base is final; # /base/foo
511 You can also drop the C<is> part of the C<is final> option if you want:
513 under base, final action foo { ... }
515 You can make end-points more visually distinct by using the C<final> keyword
516 instead of the option:
518 action base under '/';
519 final action foo under base; # /base/foo
521 And of course, the C<final>, C<under> and C<action> keywords can be used in
522 combination whenever needed:
524 action base as '' under '/';
528 final action list; # /list
534 final action view; # /list/load/view
535 final action edit; # /list/load/edit
539 There is also one shorthand alternative for declaring chain targets. You can
540 specify an action after a C<E<lt>-> following the action name:
542 action base under '/';
543 final action foo <- base; # /base/foo
547 You can use signatures like you are use to from L<MooseX::Method::Signatures>
548 to declare action parameters. The number of positinoal arguments will be used
549 during dispatching as well as their types.
551 The signature follows the action name:
554 final action foo (Int $year, Int $month, Int $day);
556 If you are using the shorthand definition, the signature follows the chain
560 final action foo <- base ($x) under '/' { ... }
562 Parameters may be specified on chain-parts and end-points:
565 action base (Str $lang) under '/';
566 final action page (Int $page_num) under base;
568 Named parameters will be populated with the values in the query parameters:
571 final action view (Int $id, Int :$page = 1) under '/';
573 Your end-points can also take an unspecified amount of arguments by specifying
574 an array as a variable:
576 # /find/some/deep/path/spec
577 final action find (@path) under '/';
581 The signatures are now validated during dispatching-time, and an action with
582 a non-matching signature (number of positional arguments and their types) will
583 not be dispatched to. This means that
585 action base under '/' as '';
589 final as double, action double_string (Str $x) {
590 $ctx->response->body( $x x 2 );
593 final as double, action double_integer (Int $x) {
594 $ctx->response->body( $x * 2 );
598 will return C<foofoo> when called as C</double/foo> and C<46> when called as
601 =head2 Actions and Method Modifiers
603 Method modifiers can not only be applied to methods, but also to actions. There
604 is no way yet to override the attributes of an already established action via
605 modifiers. However, you can modify the method underlying the action.
607 The following code is an example role modifying the consuming controller's
610 use CatalystX::Declare;
612 controller_role MyApp::Web::ControllerRole::RichBase {
614 before base (Object $ctx) {
615 $ctx->stash(something => $ctx->model('Item'));
619 Note that you have to specify the C<$ctx> argument yourself, since you are
620 modifying a method, not an action.
622 Any controller having a C<base> action (or method, for this purpose), can now
623 consume the C<RichBase> role declared above:
625 use CatalystX::Declare;
627 controller MyApp::Web::Controller::Foo
628 with MyApp::Web::Controller::RichBase {
630 action base as '' under '/';
632 action show, final under base {
633 $ctx->response->body(
634 $ctx->stash->{something}->render,
639 =head2 Action Classes
641 B<This option is even more experimental>
643 You might want to create an action with a different class than the usual
644 L<Catalyst::Action>. A usual suspect here is L<Catalyst::Action::RenderView>.
645 You can use the C<isa> option (did I mention it's experimental?) to specify
648 controller MyApp::Web::Controller::Root {
650 $CLASS->config(namespace => '');
652 action end isa RenderView;
655 The loaded class will be L<Moose>ified, so we are able to apply essential
662 =item L<MooseX::Declare::Syntax::KeywordHandling>
668 These methods are implementation details. Unless you are extending or
669 developing L<CatalystX::Declare>, you should not be concerned with them.
673 Object->parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0)
675 A hook that will be invoked by L<MooseX::Declare> when this instance is called
676 to handle syntax. It will parse the action declaration, prepare attributes and
677 add the actions to the controller.
683 =item L<CatalystX::Declare>
685 =item L<CatalystX::Declare::Keyword::Controller>
687 =item L<MooseX::Method::Signatures>
693 See L<CatalystX::Declare/AUTHOR> for author information.
697 This program is free software; you can redistribute it and/or modify it under
698 the same terms as perl itself.