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 Subname Action );
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 { ... }
401 # of course you can also chain to external actions
402 final action some_end under '/some/controller/some/action';
407 This handler class provides the user with C<action>, C<final> and C<under>
408 keywords. There are multiple ways to define actions to allow for greater
409 freedom of expression. While the parts of the action declaration itself do
410 not care about their order, their syntax is rather strict.
412 You can choose to separate syntax elements via C<,> if you think it is more
413 readable. The action declaration
415 action foo is final under base;
417 is parsed in exactly the same way if you write it as
419 action foo, is final, under base;
421 =head2 Basic Action Declaration
423 The simplest possible declaration is
427 This would define a chain-part action chained to nothing with the name C<foo>
428 and no arguments. Since it isn't followed by a block, the body of the action
431 You will automatically be provided with two variables: C<$self> is, as you
432 might expect, your controller instance. C<$ctx> will be the Catalyst context
433 object. Thus, the following code would stash the value returned by the
437 $ctx->stash(item => $self->get_item);
440 =head2 Setting a Path Part
442 As usual with Catalyst actions, the path part (the public name of this part of
443 the URI, if you're not familiar with the term yet) will default to the name of
444 the action itself (or more correctly: to whatever Catalyst defaults).
446 To change that, use the C<as> option:
449 action base as ''; # <empty>
450 action something as 'foo/bar'; # foo/bar
451 action barely as bareword; # bareword
454 =head2 Chaining Actions
456 Currently, L<CatalystX::Declare> is completely based on the concept of
457 L<chained actions|Catalyst::DispatchType::Chained>. Every action you declare is
458 chained or private. You can specify the action you want to chain to with the
461 action foo; # chained to nothing
462 action foo under '/'; # also chained to /
463 action foo under bar; # chained to the local bar action
464 action foo under '/bar/baz'; # chained to baz in /bar
466 C<under> is also provided as a grouping keyword. Every action inside the block
467 will be chained to the specified action:
474 You can also use the C<under> keyword for a single action. This is useful if
475 you want to highlight a single action with a significant diversion from what
478 action base under '/';
480 under '/the/sink' is final action foo;
482 final action bar under base;
484 final action baz under base;
486 Instead of the C<under> option declaration, you can also use a more english
487 variant named C<chains to>. While C<under> might be nice and concise, some
488 people might prefer this if they confuse C<under> with the specification of
489 a public path part. The argument to C<chains to> is the same as to C<under>:
491 action foo chains to bar;
492 action foo under bar;
494 By default all actions are chain-parts, not end-points. If you want an action
495 to be picked up as end-point and available via a public path, you have to say
496 so explicitely by using the C<is final> option:
498 action base under '/';
499 action foo under base is final; # /base/foo
501 You can also drop the C<is> part of the C<is final> option if you want:
503 under base, final action foo { ... }
505 You can make end-points more visually distinct by using the C<final> keyword
506 instead of the option:
508 action base under '/';
509 final action foo under base; # /base/foo
511 And of course, the C<final>, C<under> and C<action> keywords can be used in
512 combination whenever needed:
514 action base as '' under '/';
518 final action list; # /list
524 final action view; # /list/load/view
525 final action edit; # /list/load/edit
529 There is also one shorthand alternative for declaring chain targets. You can
530 specify an action after a C<E<lt>-> following the action name:
532 action base under '/';
533 final action foo <- base; # /base/foo
537 You can use signatures like you are use to from L<MooseX::Method::Signatures>
538 to declare action parameters. The number of arguments will be used during
539 dispatching. Dispatching by type constraint is planned but not yet implemented.
541 The signature follows the action name:
544 final action foo (Int $year, Int $month, Int $day);
546 If you are using the shorthand definition, the signature follows the chain
550 final action foo <- base ($x) under '/' { ... }
552 Parameters may be specified on chain-parts and end-points:
555 action base (Str $lang) under '/';
556 final action page (Int $page_num) under base;
558 Named parameters will be populated with the values in the query parameters:
561 final action view (Int $id, Int :$page = 1) under '/';
563 Your end-points can also take an unspecified amount of arguments by specifying
564 an array as a variable:
566 # /find/some/deep/path/spec
567 final action find (@path) under '/';
571 Currently, when the arguments do not fit the signature because of a L<Moose>
572 validation error, the response body will be set to C<Not found> and the
573 status to C<404>. This only applies when debug mode is off. If it is turned on,
574 the error message will be prefixed with C<BAD REQUEST: >. The action will
575 automatically detach after a failed signature validation.
577 =head2 Actions and Method Modifiers
579 Method modifiers can not only be applied to methods, but also to actions. There
580 is no way yet to override the attributes of an already established action via
581 modifiers. However, you can modify the method underlying the action.
583 The following code is an example role modifying the consuming controller's
586 use CatalystX::Declare;
588 controller_role MyApp::Web::ControllerRole::RichBase {
590 before base (Object $ctx) {
591 $ctx->stash(something => $ctx->model('Item'));
595 Note that you have to specify the C<$ctx> argument yourself, since you are
596 modifying a method, not an action.
598 Any controller having a C<base> action (or method, for this purpose), can now
599 consume the C<RichBase> role declared above:
601 use CatalystX::Declare;
603 controller MyApp::Web::Controller::Foo
604 with MyApp::Web::Controller::RichBase {
606 action base as '' under '/';
608 action show, final under base {
609 $ctx->response->body(
610 $ctx->stash->{something}->render,
615 =head2 Action Classes
617 B<This option is even more experimental>
619 You might want to create an action with a different class than the usual
620 L<Catalyst::Action>. A usual suspect here is L<Catalyst::Action::RenderView>.
621 You can use the C<isa> option (did I mention it's experimental?) to specify
624 controller MyApp::Web::Controller::Root {
626 $CLASS->config(namespace => '');
628 action end isa RenderView;
631 The loaded class will be L<Moose>ified, so we are able to apply essential
638 =item L<MooseX::Declare::Syntax::KeywordHandling>
644 These methods are implementation details. Unless you are extending or
645 developing L<CatalystX::Declare>, you should not be concerned with them.
649 Object->parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0)
651 A hook that will be invoked by L<MooseX::Declare> when this instance is called
652 to handle syntax. It will parse the action declaration, prepare attributes and
653 add the actions to the controller.
659 =item L<CatalystX::Declare>
661 =item L<CatalystX::Declare::Keyword::Controller>
663 =item L<MooseX::Method::Signatures>
669 See L<CatalystX::Declare/AUTHOR> for author information.
673 This program is free software; you can redistribute it and/or modify it under
674 the same terms as perl itself.