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 Setting a Path Part
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).
450 To change that, use the C<as> option:
453 action base as ''; # <empty>
454 action something as 'foo/bar'; # foo/bar
455 action barely as bareword; # bareword
458 =head2 Chaining Actions
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
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
470 C<under> is also provided as a grouping keyword. Every action inside the block
471 will be chained to the specified action:
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
482 action base under '/';
484 under '/the/sink' is final action foo;
486 final action bar under base;
488 final action baz under base;
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>:
495 action foo chains to bar;
496 action foo under bar;
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:
502 action base under '/';
503 action foo under base is final; # /base/foo
505 You can also drop the C<is> part of the C<is final> option if you want:
507 under base, final action foo { ... }
509 You can make end-points more visually distinct by using the C<final> keyword
510 instead of the option:
512 action base under '/';
513 final action foo under base; # /base/foo
515 And of course, the C<final>, C<under> and C<action> keywords can be used in
516 combination whenever needed:
518 action base as '' under '/';
522 final action list; # /list
528 final action view; # /list/load/view
529 final action edit; # /list/load/edit
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:
536 action base under '/';
537 final action foo <- base; # /base/foo
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.
545 The signature follows the action name:
548 final action foo (Int $year, Int $month, Int $day);
550 If you are using the shorthand definition, the signature follows the chain
554 final action foo <- base ($x) under '/' { ... }
556 Parameters may be specified on chain-parts and end-points:
559 action base (Str $lang) under '/';
560 final action page (Int $page_num) under base;
562 Named parameters will be populated with the values in the query parameters:
565 final action view (Int $id, Int :$page = 1) under '/';
567 Your end-points can also take an unspecified amount of arguments by specifying
568 an array as a variable:
570 # /find/some/deep/path/spec
571 final action find (@path) under '/';
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
579 action base under '/' as '';
583 final as double, action double_string (Str $x) {
584 $ctx->response->body( $x x 2 );
587 final as double, action double_integer (Int $x) {
588 $ctx->response->body( $x * 2 );
592 will return C<foofoo> when called as C</double/foo> and C<46> when called as
595 =head2 Actions and Method Modifiers
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.
601 The following code is an example role modifying the consuming controller's
604 use CatalystX::Declare;
606 controller_role MyApp::Web::ControllerRole::RichBase {
608 before base (Object $ctx) {
609 $ctx->stash(something => $ctx->model('Item'));
613 Note that you have to specify the C<$ctx> argument yourself, since you are
614 modifying a method, not an action.
616 Any controller having a C<base> action (or method, for this purpose), can now
617 consume the C<RichBase> role declared above:
619 use CatalystX::Declare;
621 controller MyApp::Web::Controller::Foo
622 with MyApp::Web::Controller::RichBase {
624 action base as '' under '/';
626 action show, final under base {
627 $ctx->response->body(
628 $ctx->stash->{something}->render,
633 =head2 Action Classes
635 B<This option is even more experimental>
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
642 controller MyApp::Web::Controller::Root {
644 $CLASS->config(namespace => '');
646 action end isa RenderView;
649 The loaded class will be L<Moose>ified, so we are able to apply essential
656 =item L<MooseX::Declare::Syntax::KeywordHandling>
662 These methods are implementation details. Unless you are extending or
663 developing L<CatalystX::Declare>, you should not be concerned with them.
667 Object->parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0)
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.
677 =item L<CatalystX::Declare>
679 =item L<CatalystX::Declare::Keyword::Controller>
681 =item L<MooseX::Method::Signatures>
687 See L<CatalystX::Declare/AUTHOR> for author information.
691 This program is free software; you can redistribute it and/or modify it under
692 the same terms as perl itself.