Commit | Line | Data |
918fb36e |
1 | use MooseX::Declare; |
c2a8165b |
2 | use MooseX::Role::Parameterized (); |
918fb36e |
3 | |
8d66ec34 |
4 | class CatalystX::Declare::Keyword::Action { |
918fb36e |
5 | |
6 | |
a1dd1788 |
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 ); |
c2a8165b |
11 | use Moose::Util qw( add_method_modifier ensure_all_roles ); |
a82dd4d2 |
12 | use Data::Pond qw(pond_write_datum); |
a1dd1788 |
13 | use Class::Inspector; |
14 | use Class::MOP; |
15 | |
918fb36e |
16 | |
e10b92dd |
17 | use constant STOP_PARSING => '__MXDECLARE_STOP_PARSING__'; |
9c11a562 |
18 | use constant UNDER_VAR => '$CatalystX::Declare::SCOPE::UNDER'; |
fe864e80 |
19 | use constant UNDER_STACK => '@CatalystX::Declare::SCOPE::UNDER_STACK'; |
e10b92dd |
20 | |
5fb5cef1 |
21 | use aliased 'CatalystX::Declare::Action::CatchValidationError'; |
8d66ec34 |
22 | use aliased 'CatalystX::Declare::Context::StringParsing'; |
918fb36e |
23 | use aliased 'MooseX::Method::Signatures::Meta::Method'; |
24 | use aliased 'MooseX::MethodAttributes::Role::Meta::Method', 'AttributeRole'; |
c2a8165b |
25 | use aliased 'MooseX::MethodAttributes::Role::Meta::Role', 'AttributeMetaRole'; |
918fb36e |
26 | |
27 | |
856ac9a7 |
28 | method parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0) { |
918fb36e |
29 | |
30 | # somewhere to put the attributes |
31 | my %attributes; |
32 | my @populators; |
918fb36e |
33 | |
34 | # parse declarations |
35 | until (do { $ctx->skipspace; $ctx->peek_next_char } eq any qw( ; { } )) { |
918fb36e |
36 | |
37 | $ctx->skipspace; |
38 | |
39 | # optional commas |
40 | if ($ctx->peek_next_char eq ',') { |
41 | |
42 | my $linestr = $ctx->get_linestr; |
43 | substr($linestr, $ctx->offset, 1) = ''; |
44 | $ctx->set_linestr($linestr); |
45 | |
46 | next; |
47 | } |
48 | |
49 | # next thing should be an option name |
50 | my $option = ( |
51 | $skipped_declarator |
52 | ? $ctx->strip_name |
53 | : do { |
54 | $ctx->skip_declarator; |
55 | $skipped_declarator++; |
56 | $ctx->declarator; |
57 | }) |
58 | or croak "Expected option token, not " . substr($ctx->get_linestr, $ctx->offset); |
59 | |
60 | # we need to be able to handle the rest |
61 | my $handler = $self->can("_handle_${option}_option") |
62 | or croak "Unknown action option: $option"; |
63 | |
64 | # call the handler |
e10b92dd |
65 | my $populator = $self->$handler($ctx, \%attributes); |
66 | |
67 | if ($populator and $populator eq STOP_PARSING) { |
68 | |
69 | return $ctx->shadow(sub (&) { |
70 | my ($body) = @_; |
71 | return $body->(); |
72 | }); |
73 | } |
74 | |
75 | push @populators, $populator |
76 | if defined $populator; |
918fb36e |
77 | } |
78 | |
79 | croak "Need an action specification" |
80 | unless exists $attributes{Signature}; |
81 | |
82 | my $name = $attributes{Subname}; |
856ac9a7 |
83 | |
17a275f5 |
84 | if ($attributes{Private}) { |
85 | $attributes{Signature} ||= '@'; |
86 | } |
87 | |
918fb36e |
88 | my $method = Method->wrap( |
89 | signature => qq{($attributes{Signature})}, |
90 | package_name => $ctx->get_curstash_name, |
91 | name => $name, |
92 | ); |
93 | |
a1dd1788 |
94 | AttributeRole->meta->apply($method); |
95 | |
eb97acbb |
96 | my $count = $self->_count_positional_arguments($method); |
97 | $attributes{CaptureArgs} = $count |
98 | if defined $count; |
99 | |
918fb36e |
100 | $_->($method) |
101 | for @populators; |
102 | |
aae7ad1f |
103 | unless ($attributes{Private}) { |
dd2759b0 |
104 | $attributes{PathPart} ||= $name; |
918fb36e |
105 | |
aae7ad1f |
106 | delete $attributes{CaptureArgs} |
107 | if exists $attributes{Args}; |
918fb36e |
108 | |
aae7ad1f |
109 | $attributes{CaptureArgs} = 0 |
110 | unless exists $attributes{Args} |
111 | or exists $attributes{CaptureArgs}; |
112 | } |
113 | |
114 | if ($attributes{Private}) { |
aae7ad1f |
115 | delete $attributes{ $_ } |
ed4a2203 |
116 | for qw( Args CaptureArgs Chained Signature Private ); |
aae7ad1f |
117 | } |
918fb36e |
118 | |
4960c7ec |
119 | # inject a hashref for resolving runtime attribute values |
8d66ec34 |
120 | $self->_inject_attributes($ctx, \%attributes); |
121 | |
4960c7ec |
122 | # our declaration is followed by a block |
918fb36e |
123 | if ($ctx->peek_next_char eq '{') { |
124 | $ctx->inject_if_block($ctx->scope_injector_call . $method->injectable_code); |
125 | } |
4960c7ec |
126 | |
127 | # there is no block, so we insert one. |
918fb36e |
128 | else { |
129 | $ctx->inject_code_parts_here( |
130 | sprintf '{ %s%s }', |
131 | $ctx->scope_injector_call, |
132 | $method->injectable_code, |
133 | ); |
134 | } |
135 | |
8d66ec34 |
136 | my $compile_attrs = sub { |
137 | my $attributes = shift;; |
138 | my @attributes; |
139 | |
140 | for my $attr (keys %$attributes) { |
141 | my $value = $attributes->{ $attr }; |
142 | |
4960c7ec |
143 | # the compiletime chained attr might contain the under global var name |
8d66ec34 |
144 | next if $attr eq 'Chained' and $value eq UNDER_VAR; |
145 | |
8d66ec34 |
146 | push @attributes, |
a82dd4d2 |
147 | map { |
148 | my $value = ref $_ ? pond_write_datum($_) : $_; |
149 | sprintf '%s%s', $attr, defined($value) ? sprintf('(%s)', $value) : '' |
150 | } ( |
151 | ref($value) eq 'ARRAY' |
152 | ) ? @$value : $value; |
8d66ec34 |
153 | } |
154 | |
155 | return \@attributes; |
156 | }; |
918fb36e |
157 | |
8d66ec34 |
158 | return $ctx->shadow(sub { |
918fb36e |
159 | my $class = caller; |
8d66ec34 |
160 | my $attrs = shift; |
856ac9a7 |
161 | my $body = shift; |
4960c7ec |
162 | |
163 | # the runtime-resolved name |
dd2759b0 |
164 | my $name = $attrs->{Subname}; |
918fb36e |
165 | |
4960c7ec |
166 | # in case no hashref was specified |
8d66ec34 |
167 | $body = $attrs and $attrs = {} |
168 | if ref $attrs eq 'CODE'; |
169 | |
4960c7ec |
170 | # default path part to runtime-resolved name |
dd2759b0 |
171 | unless ($attrs->{Private}) { |
172 | |
173 | $attrs->{PathPart} = $attrs->{Subname} |
174 | unless defined $attrs->{PathPart}; |
175 | } |
176 | |
4960c7ec |
177 | # in CXD we are explicit about chained values, an undefined |
178 | # value means we defaulted to the outer-scope under and there |
179 | # was none. |
8d66ec34 |
180 | delete $attrs->{Chained} |
181 | unless defined $attrs->{Chained}; |
182 | |
4960c7ec |
183 | # some attrs need to be single quoted in their stringified forms |
8d66ec34 |
184 | defined($attrs->{ $_ }) and $attrs->{ $_ } = sprintf "'%s'", $attrs->{ $_ } |
185 | for qw( Chained PathPart ); |
186 | |
4960c7ec |
187 | # merge runtime and compiletime attrs |
8d66ec34 |
188 | my %full_attrs = (%attributes, %$attrs); |
8d66ec34 |
189 | my $compiled_attrs = $compile_attrs->(\%full_attrs); |
24a5fc45 |
190 | my $real_method = $method->reify( |
191 | actual_body => $body, |
8d66ec34 |
192 | attributes => $compiled_attrs, |
dd2759b0 |
193 | name => $name, |
24a5fc45 |
194 | ); |
856ac9a7 |
195 | |
4960c7ec |
196 | # NYI |
856ac9a7 |
197 | if ($modifier) { |
24a5fc45 |
198 | add_method_modifier $class, $modifier, [$name, $real_method]; |
b061ac27 |
199 | } |
200 | else { |
856ac9a7 |
201 | |
c2a8165b |
202 | my $prepare_meta = sub { |
203 | my ($meta) = @_; |
204 | |
24a5fc45 |
205 | $meta->add_method($name, $real_method); |
8d66ec34 |
206 | $meta->register_method_attributes($meta->name->can($real_method->name), $compiled_attrs); |
c2a8165b |
207 | }; |
208 | |
209 | if ($ctx->stack->[-1] and $ctx->stack->[-1]->is_parameterized) { |
210 | my $real_meta = MooseX::Role::Parameterized->current_metaclass; |
211 | |
212 | $real_meta->meta->make_mutable |
213 | if $real_meta->meta->is_immutable; |
214 | ensure_all_roles $real_meta->meta, AttributeMetaRole |
215 | if $real_meta->isa('Moose::Meta::Role'); |
216 | |
217 | $real_meta->$prepare_meta; |
218 | } |
1754e3e7 |
219 | else { |
1754e3e7 |
220 | $class->meta->$prepare_meta; |
221 | } |
856ac9a7 |
222 | } |
918fb36e |
223 | }); |
224 | } |
225 | |
a1dd1788 |
226 | method _handle_with_option (Object $ctx, HashRef $attrs) { |
227 | |
284b2994 |
228 | my @roles_with_args = (); |
229 | push @roles_with_args, @{ $ctx->strip_names_and_args }; |
a1dd1788 |
230 | |
231 | # we need to fish for aliases here since we are still unclean |
284b2994 |
232 | my @roles = (); |
233 | for my $role_with_arg(@roles_with_args) { |
d14acb87 |
234 | my ($role, $params) = @{$role_with_arg}; |
235 | if($params) { |
236 | my ($first, @rest) = eval $params; |
34a0a1ff |
237 | my %params = ref $first eq 'HASH' ? %$first : ($first, @rest); # both (%opts) and {%opts} |
d14acb87 |
238 | for my $key (keys %params) { |
a82dd4d2 |
239 | my $value = ref $params{$key} eq 'ARRAY' ? $params{$key} : [$params{$key}]; |
240 | push @{ $attrs->{$key} ||=[] }, @$value; |
241 | ##push @{ $attrs->{$key} ||=[] }, $params; |
242 | |
d14acb87 |
243 | } |
244 | } |
245 | |
284b2994 |
246 | if (defined(my $alias = $self->_check_for_available_import($ctx, $role))) { |
247 | $role = $alias; |
248 | } |
249 | push @roles, $role; |
a1dd1788 |
250 | } |
251 | |
284b2994 |
252 | push @{ $attrs->{CatalystX_Declarative_ActionRoles} ||= [] }, @roles; |
a1dd1788 |
253 | return; |
254 | } |
255 | |
256 | method _handle_isa_option (Object $ctx, HashRef $attrs) { |
257 | |
258 | my $class = $ctx->strip_name |
259 | or croak "Expected bareword action class specification for action after isa"; |
260 | |
261 | if (defined(my $alias = $self->_check_for_available_import($ctx, $class))) { |
262 | $class = $alias; |
263 | } |
264 | |
265 | $attrs->{CatalystX_Declarative_ActionClass} = $class; |
266 | |
267 | return; |
268 | } |
269 | |
270 | method _check_for_available_import (Object $ctx, Str $name) { |
271 | |
272 | if (my $code = $ctx->get_curstash_name->can($name)) { |
273 | return $code->(); |
274 | } |
275 | |
276 | return undef; |
277 | } |
278 | |
918fb36e |
279 | method _handle_action_option (Object $ctx, HashRef $attrs) { |
280 | |
281 | # action name |
dd2759b0 |
282 | my $name = $self->_strip_actionpath($ctx, interpolate => 1) |
918fb36e |
283 | or croak "Anonymous actions not yet supported"; |
284 | |
64baeca0 |
285 | $ctx->skipspace; |
286 | my $populator; |
287 | |
4960c7ec |
288 | # shortcut under base option is basically handled by the under handler |
64baeca0 |
289 | if (substr($ctx->get_linestr, $ctx->offset, 2) eq '<-') { |
290 | my $linestr = $ctx->get_linestr; |
291 | substr($linestr, $ctx->offset, 2) = ''; |
292 | $ctx->set_linestr($linestr); |
293 | $populator = $self->_handle_under_option($ctx, $attrs); |
294 | } |
295 | |
918fb36e |
296 | # signature |
297 | my $proto = $ctx->strip_proto || ''; |
298 | $proto = join(', ', 'Object $self: Object $ctx', $proto || ()); |
299 | |
300 | $attrs->{Subname} = $name; |
301 | $attrs->{Signature} = $proto; |
aae7ad1f |
302 | $attrs->{Action} = []; |
918fb36e |
303 | |
12b853ff |
304 | push @{ $attrs->{CatalystX_Declarative_DefaultActionRoles} ||= [] }, CatchValidationError; |
4960c7ec |
305 | |
306 | # default chained base to the global under var, to be resolved at runtime |
8d66ec34 |
307 | $attrs->{Chained} ||= UNDER_VAR; |
e10b92dd |
308 | |
64baeca0 |
309 | return unless $populator; |
310 | return $populator; |
918fb36e |
311 | } |
312 | |
2dde75e7 |
313 | method _handle_final_option (Object $ctx, HashRef $attrs) { |
314 | |
315 | return $self->_build_flag_populator($ctx, $attrs, 'final'); |
316 | } |
317 | |
918fb36e |
318 | method _handle_is_option (Object $ctx, HashRef $attrs) { |
319 | |
320 | my $what = $ctx->strip_name |
321 | or croak "Expected symbol token after is symbol, not " . substr($ctx->get_linestr, $ctx->offset); |
322 | |
2dde75e7 |
323 | return $self->_build_flag_populator($ctx, $attrs, $what); |
324 | } |
325 | |
326 | method _build_flag_populator (Object $ctx, HashRef $attrs, Str $what) { |
327 | |
17a275f5 |
328 | $attrs->{Private} = [] |
329 | if $what eq 'private'; |
330 | |
918fb36e |
331 | return sub { |
332 | my $method = shift; |
333 | |
334 | if ($what eq any qw( end endpoint final )) { |
eb97acbb |
335 | $attrs->{Args} = delete $attrs->{CaptureArgs}; |
918fb36e |
336 | } |
337 | elsif ($what eq 'private') { |
aae7ad1f |
338 | $attrs->{Private} = []; |
918fb36e |
339 | } |
340 | }; |
341 | } |
342 | |
343 | method _handle_under_option (Object $ctx, HashRef $attrs) { |
344 | |
8d66ec34 |
345 | my $target = $self->_strip_actionpath($ctx, interpolate => 1); |
e10b92dd |
346 | $ctx->skipspace; |
347 | |
348 | if ($ctx->peek_next_char eq '{' and $self->identifier eq 'under') { |
349 | $ctx->inject_if_block( |
8d66ec34 |
350 | $ctx->scope_injector_call . |
351 | sprintf ';local %s = %s;', |
e10b92dd |
352 | UNDER_VAR, |
353 | $target, |
354 | ); |
355 | return STOP_PARSING; |
356 | } |
357 | |
8d66ec34 |
358 | $attrs->{Chained} = $target; |
918fb36e |
359 | |
360 | return sub { |
361 | my $method = shift; |
918fb36e |
362 | }; |
363 | } |
364 | |
365 | method _handle_chains_option (Object $ctx, HashRef $attrs) { |
366 | |
367 | $ctx->skipspace; |
368 | $ctx->strip_name eq 'to' |
369 | or croak "Expected to token after chains symbol, not " . substr($ctx->get_linestr, $ctx->offset); |
370 | |
371 | return $self->_handle_under_option($ctx, $attrs); |
372 | } |
373 | |
374 | method _handle_as_option (Object $ctx, HashRef $attrs) { |
375 | |
376 | $ctx->skipspace; |
377 | |
8d66ec34 |
378 | my $path = $self->_strip_actionpath($ctx, interpolate => 1); |
379 | $attrs->{PathPart} = $path; |
918fb36e |
380 | |
381 | return; |
382 | } |
383 | |
384 | method _count_positional_arguments (Object $method) { |
c2a8165b |
385 | my $signature = $method->parsed_signature; |
918fb36e |
386 | |
387 | if ($signature->has_positional_params) { |
388 | my $count = @{ scalar($signature->positional_params) }; |
389 | |
390 | if ($count and ($signature->positional_params)[-1]->sigil eq '@') { |
391 | return undef; |
392 | } |
393 | |
394 | return $count - 1; |
395 | } |
396 | |
397 | return 0; |
398 | } |
399 | |
8d66ec34 |
400 | method _inject_attributes (Object $ctx, HashRef $attrs) { |
401 | |
4960c7ec |
402 | # attrs that need to be runtime-resolved |
dd2759b0 |
403 | my @inject = qw( Chained PathPart Subname ); |
8d66ec34 |
404 | |
4960c7ec |
405 | # turn specific attributes into a hashref |
406 | my $code = sprintf ' +{ %s }, sub ', # the ', sub ' turns method +{ ... } { ... } into |
407 | join ', ', # method +{ ... }, sub { ... } |
8d66ec34 |
408 | map { (@$_) } |
8d66ec34 |
409 | map { defined( $_->[1] ) ? $_ : [$_->[0], 'undef'] } |
410 | map { [pp($_), $attrs->{ $_ }] } |
411 | grep { defined $attrs->{ $_ } } |
412 | @inject; |
413 | |
4960c7ec |
414 | # inject the hashref code before the action body |
8d66ec34 |
415 | $ctx->inject_code_parts_here($code); |
416 | $ctx->inc_offset(length $code); |
417 | } |
418 | |
419 | method _strip_actionpath (Object $ctx, :$interpolate?) { |
918fb36e |
420 | |
421 | $ctx->skipspace; |
422 | my $linestr = $ctx->get_linestr; |
423 | my $rest = substr($linestr, $ctx->offset); |
8d66ec34 |
424 | my $interp = sub { $interpolate ? "'$_[0]'" : $_[0] }; |
918fb36e |
425 | |
4960c7ec |
426 | # find simple barewords |
918fb36e |
427 | if ($rest =~ /^ ( [_a-z] [_a-z0-9]* ) \b/ix) { |
428 | substr($linestr, $ctx->offset, length($1)) = ''; |
429 | $ctx->set_linestr($linestr); |
8d66ec34 |
430 | return $interp->($1); |
918fb36e |
431 | } |
4960c7ec |
432 | |
433 | # allow single quoted more complex barewords |
a0ebba1d |
434 | elsif ($rest =~ /^ ' ( (?:[.:;,_a-z0-9]|\/)* ) ' /ix) { |
918fb36e |
435 | substr($linestr, $ctx->offset, length($1) + 2) = ''; |
436 | $ctx->set_linestr($linestr); |
8d66ec34 |
437 | return $interp->($1); |
438 | } |
4960c7ec |
439 | |
440 | # double quoted strings and variables |
8d66ec34 |
441 | elsif ($interpolate and my $str = $ctx->get_string) { |
442 | return $str; |
918fb36e |
443 | } |
4960c7ec |
444 | |
445 | # not suitable as action path |
918fb36e |
446 | else { |
447 | croak "Invalid syntax for action path: $rest"; |
448 | } |
449 | } |
8d66ec34 |
450 | |
4960c7ec |
451 | # down here because it requires the parse method |
8d66ec34 |
452 | with 'MooseX::Declare::Syntax::KeywordHandling'; |
453 | |
454 | around context_traits { $self->$orig, StringParsing } |
918fb36e |
455 | } |
456 | |
856ac9a7 |
457 | __END__ |
458 | |
459 | =head1 NAME |
460 | |
461 | CatalystX::Declare::Keyword::Action - Declare Catalyst Actions |
462 | |
463 | =head1 SYNOPSIS |
464 | |
465 | use CatalystX::Declare; |
466 | |
467 | controller MyApp::Web::Controller::Example { |
468 | |
469 | # chain base action with path part setting of '' |
470 | # body-less actions don't do anything by themselves |
6e2492a4 |
471 | action base as '' under '/'; |
856ac9a7 |
472 | |
473 | # simple end-point action |
474 | action controller_class is final under base { |
475 | $ctx->response->body( 'controller: ' . ref $self ); |
476 | } |
477 | |
478 | # chain part actions can have arguments |
479 | action str (Str $string) under base { |
480 | |
481 | $ctx->stash(chars => [split //, $string]); |
482 | } |
483 | |
484 | # and end point actions too, of course |
485 | action uc_chars (Int $count) under str is final { |
486 | |
487 | my $chars = $ctx->stash->{chars}; |
488 | ... |
489 | } |
490 | |
491 | |
492 | # you can use a shortcut for multiple actions with |
493 | # a common base |
494 | under base { |
495 | |
496 | # this is an endpoint after base |
497 | action normal is final; |
498 | |
499 | # the final keyword can be used to be more |
500 | # visually explicit about end-points |
501 | final action some_action { ... } |
ed4a2203 |
502 | |
503 | # type dispatching works |
504 | final action with_str (Str $x) as via_type; |
505 | final action with_int (Int $x) as via_type; |
856ac9a7 |
506 | } |
507 | |
508 | # of course you can also chain to external actions |
509 | final action some_end under '/some/controller/some/action'; |
510 | } |
511 | |
512 | =head1 DESCRIPTION |
513 | |
514 | This handler class provides the user with C<action>, C<final> and C<under> |
515 | keywords. There are multiple ways to define actions to allow for greater |
516 | freedom of expression. While the parts of the action declaration itself do |
517 | not care about their order, their syntax is rather strict. |
518 | |
519 | You can choose to separate syntax elements via C<,> if you think it is more |
520 | readable. The action declaration |
521 | |
522 | action foo is final under base; |
523 | |
524 | is parsed in exactly the same way if you write it as |
525 | |
526 | action foo, is final, under base; |
527 | |
528 | =head2 Basic Action Declaration |
529 | |
530 | The simplest possible declaration is |
531 | |
532 | action foo; |
533 | |
6e2492a4 |
534 | This would define a chain-part action chained to nothing with the name C<foo> |
856ac9a7 |
535 | and no arguments. Since it isn't followed by a block, the body of the action |
536 | will be empty. |
537 | |
538 | You will automatically be provided with two variables: C<$self> is, as you |
539 | might expect, your controller instance. C<$ctx> will be the Catalyst context |
540 | object. Thus, the following code would stash the value returned by the |
541 | C<get_item> method: |
542 | |
543 | action foo { |
544 | $ctx->stash(item => $self->get_item); |
545 | } |
546 | |
32663314 |
547 | =head2 Why $ctx instead of $c |
548 | |
549 | Some might ask why the context object is called C<$ctx> instead of the usual |
550 | C<$c>. The reason is simple: It's an opinionated best practice, since C<$ctx> |
551 | stands out more. |
552 | |
856ac9a7 |
553 | =head2 Setting a Path Part |
554 | |
555 | As usual with Catalyst actions, the path part (the public name of this part of |
556 | the URI, if you're not familiar with the term yet) will default to the name of |
557 | the action itself (or more correctly: to whatever Catalyst defaults). |
558 | |
559 | To change that, use the C<as> option: |
560 | |
6e2492a4 |
561 | under something { |
562 | action base as ''; # <empty> |
563 | action something as 'foo/bar'; # foo/bar |
564 | action barely as bareword; # bareword |
565 | } |
856ac9a7 |
566 | |
567 | =head2 Chaining Actions |
568 | |
569 | Currently, L<CatalystX::Declare> is completely based on the concept of |
570 | L<chained actions|Catalyst::DispatchType::Chained>. Every action you declare is |
6e2492a4 |
571 | chained or private. You can specify the action you want to chain to with the |
572 | C<under> option: |
856ac9a7 |
573 | |
6e2492a4 |
574 | action foo; # chained to nothing |
856ac9a7 |
575 | action foo under '/'; # also chained to / |
576 | action foo under bar; # chained to the local bar action |
577 | action foo under '/bar/baz'; # chained to baz in /bar |
578 | |
579 | C<under> is also provided as a grouping keyword. Every action inside the block |
580 | will be chained to the specified action: |
581 | |
582 | under base { |
583 | action foo { ... } |
584 | action bar { ... } |
585 | } |
586 | |
587 | You can also use the C<under> keyword for a single action. This is useful if |
588 | you want to highlight a single action with a significant diversion from what |
589 | is to be expected: |
590 | |
6e2492a4 |
591 | action base under '/'; |
856ac9a7 |
592 | |
593 | under '/the/sink' is final action foo; |
594 | |
595 | final action bar under base; |
596 | |
597 | final action baz under base; |
598 | |
599 | Instead of the C<under> option declaration, you can also use a more english |
600 | variant named C<chains to>. While C<under> might be nice and concise, some |
601 | people might prefer this if they confuse C<under> with the specification of |
602 | a public path part. The argument to C<chains to> is the same as to C<under>: |
603 | |
604 | action foo chains to bar; |
605 | action foo under bar; |
606 | |
607 | By default all actions are chain-parts, not end-points. If you want an action |
608 | to be picked up as end-point and available via a public path, you have to say |
609 | so explicitely by using the C<is final> option: |
610 | |
6e2492a4 |
611 | action base under '/'; |
856ac9a7 |
612 | action foo under base is final; # /base/foo |
613 | |
614 | You can also drop the C<is> part of the C<is final> option if you want: |
615 | |
616 | under base, final action foo { ... } |
617 | |
618 | You can make end-points more visually distinct by using the C<final> keyword |
619 | instead of the option: |
620 | |
6e2492a4 |
621 | action base under '/'; |
856ac9a7 |
622 | final action foo under base; # /base/foo |
623 | |
624 | And of course, the C<final>, C<under> and C<action> keywords can be used in |
625 | combination whenever needed: |
626 | |
6e2492a4 |
627 | action base as '' under '/'; |
856ac9a7 |
628 | |
629 | under base { |
630 | |
631 | final action list; # /list |
632 | |
633 | action load; |
634 | |
635 | under load { |
636 | |
637 | final action view; # /list/load/view |
638 | final action edit; # /list/load/edit |
639 | } |
640 | } |
641 | |
642 | There is also one shorthand alternative for declaring chain targets. You can |
643 | specify an action after a C<E<lt>-> following the action name: |
644 | |
6e2492a4 |
645 | action base under '/'; |
856ac9a7 |
646 | final action foo <- base; # /base/foo |
647 | |
648 | =head2 Arguments |
649 | |
650 | You can use signatures like you are use to from L<MooseX::Method::Signatures> |
ed4a2203 |
651 | to declare action parameters. The number of positinoal arguments will be used |
652 | during dispatching as well as their types. |
856ac9a7 |
653 | |
654 | The signature follows the action name: |
655 | |
656 | # /foo/*/*/* |
657 | final action foo (Int $year, Int $month, Int $day); |
658 | |
659 | If you are using the shorthand definition, the signature follows the chain |
660 | target: |
661 | |
662 | # /foo/* |
6e2492a4 |
663 | final action foo <- base ($x) under '/' { ... } |
856ac9a7 |
664 | |
665 | Parameters may be specified on chain-parts and end-points: |
666 | |
667 | # /base/*/foo/* |
6e2492a4 |
668 | action base (Str $lang) under '/'; |
856ac9a7 |
669 | final action page (Int $page_num) under base; |
670 | |
671 | Named parameters will be populated with the values in the query parameters: |
672 | |
673 | # /view/17/?page=3 |
6e2492a4 |
674 | final action view (Int $id, Int :$page = 1) under '/'; |
856ac9a7 |
675 | |
accfac7d |
676 | If you specify a query parameter to be an C<ArrayRef>, it will be specially |
677 | handled. For one, it will match even if there is no such value in the |
678 | parameters. Second, it will always be wrapped as an array reference. |
679 | |
856ac9a7 |
680 | Your end-points can also take an unspecified amount of arguments by specifying |
681 | an array as a variable: |
682 | |
683 | # /find/some/deep/path/spec |
6e2492a4 |
684 | final action find (@path) under '/'; |
856ac9a7 |
685 | |
5fb5cef1 |
686 | =head2 Validation |
687 | |
ed4a2203 |
688 | The signatures are now validated during dispatching-time, and an action with |
689 | a non-matching signature (number of positional arguments and their types) will |
690 | not be dispatched to. This means that |
691 | |
692 | action base under '/' as ''; |
693 | |
694 | under base { |
695 | |
ed4a2203 |
696 | final as double, action double_integer (Int $x) { |
697 | $ctx->response->body( $x * 2 ); |
698 | } |
aee1c364 |
699 | |
700 | final as double, action double_string (Str $x) { |
701 | $ctx->response->body( $x x 2 ); |
702 | } |
ed4a2203 |
703 | } |
704 | |
705 | will return C<foofoo> when called as C</double/foo> and C<46> when called as |
706 | C</double/23>. |
5fb5cef1 |
707 | |
856ac9a7 |
708 | =head2 Actions and Method Modifiers |
709 | |
710 | Method modifiers can not only be applied to methods, but also to actions. There |
711 | is no way yet to override the attributes of an already established action via |
712 | modifiers. However, you can modify the method underlying the action. |
713 | |
714 | The following code is an example role modifying the consuming controller's |
715 | C<base> action: |
716 | |
717 | use CatalystX::Declare; |
718 | |
205323ac |
719 | controller_role MyApp::Web::ControllerRole::RichBase { |
856ac9a7 |
720 | |
721 | before base (Object $ctx) { |
722 | $ctx->stash(something => $ctx->model('Item')); |
723 | } |
724 | } |
725 | |
726 | Note that you have to specify the C<$ctx> argument yourself, since you are |
727 | modifying a method, not an action. |
728 | |
729 | Any controller having a C<base> action (or method, for this purpose), can now |
730 | consume the C<RichBase> role declared above: |
731 | |
732 | use CatalystX::Declare; |
733 | |
734 | controller MyApp::Web::Controller::Foo |
735 | with MyApp::Web::Controller::RichBase { |
736 | |
6e2492a4 |
737 | action base as '' under '/'; |
856ac9a7 |
738 | |
739 | action show, final under base { |
740 | $ctx->response->body( |
741 | $ctx->stash->{something}->render, |
742 | ); |
743 | } |
744 | } |
745 | |
72a81644 |
746 | You can consume multiple action roles similarly to the way you do with the |
747 | class or role keyword: |
748 | |
749 | action user |
750 | with LoggedIn |
751 | with isSuperUser {} |
752 | |
753 | Or |
754 | |
755 | action User |
756 | with (LoggedIn, isSuperUser) {} |
757 | |
758 | Lastly, you can pass parameters to the underlying L<Catalyst::Action> using |
759 | a syntax that is similar to method traits: |
760 | |
761 | action myaction with hasRole(opt1=>'val1', opt2=>'val2') |
762 | |
763 | Where C<%opts> is a hash that is used to populate $action->attributes in the |
764 | same way you might have done the following in classic L<Catalyst> |
765 | |
766 | sub myaction :Action :Does(hasRole) :opt1(val1) :opt2(val2) |
767 | |
768 | Here's a more detailed example: |
769 | |
770 | action User |
771 | with hasLogger(log_engine=>'STDOUT') |
772 | with hasPermissions( |
773 | role=>['Administrator', 'Member'], |
774 | ) {} |
775 | |
776 | Think of these are classic catalyst subroutine attributes on steriods. Unlike |
777 | subroutine attributes, you can split and format your code across multiple lines |
778 | and you can use deep and complex data structures such as HashRefs or ArrayRefs. |
779 | Also, since the parameters are grouped syntactically within the C<with> keyword |
780 | this should improve readability of your code, since it will be more clear which |
781 | parameters belong to with roles. This should give L<CatalystX::Declare> greater |
782 | compatibility with legacy L<Catalyst> code but offer us a way forward from |
783 | needing subroutine attributes, which suffer from significant drawbacks. |
784 | |
785 | A few caveats and differences from method traits. First of all, unlike method |
786 | traits, parameters are not passed to the L<Catalyst::Action> constructor, but |
787 | instead used to populate the C<attributes> attribute, which is to preserve |
788 | compatibility with how subroutine attributes work in classic L<Catalyst>. |
789 | |
790 | Additionally, since subroutines attributes supported a very limited syntax for |
791 | supplying values, we follow the convention where parameter values are pushed |
792 | onto an arrayref. In other words the following: |
793 | |
794 | action User with hasLogger(engine=>'STDOUT') |
795 | |
796 | would create the following data structure: |
797 | |
798 | $action->attributes->{engine} = ['STDOUT'] |
799 | |
800 | The one exception is that if the value is an arrayref, those will be merged: |
801 | |
802 | action User with Permissions(roles=>[qw/admin member/]) {} |
803 | ## Creates: $action->attributes->{roles} = ['admin','member'] |
804 | |
805 | My feeling is that this gives better backward compatibility with classic sub |
806 | attributes: |
807 | |
808 | sub User :Action :Does(Permissions) :roles(admin) :roles(member) |
809 | |
b09fbf67 |
810 | However, I realize this method could lead to namespace collisions within the |
811 | C<$action->attributes> attribute. For now this is an avoidable issue. In the |
812 | future we may add a C<$action->trait_attributes> or similar attribute to the |
813 | L<Catalyst::Action> class in order to resolve this issue. |
72a81644 |
814 | |
2bb54af3 |
815 | =head2 Action Classes |
816 | |
817 | B<This option is even more experimental> |
818 | |
819 | You might want to create an action with a different class than the usual |
820 | L<Catalyst::Action>. A usual suspect here is L<Catalyst::Action::RenderView>. |
821 | You can use the C<isa> option (did I mention it's experimental?) to specify |
822 | what class to use: |
823 | |
824 | controller MyApp::Web::Controller::Root { |
825 | |
826 | $CLASS->config(namespace => ''); |
827 | |
828 | action end isa RenderView; |
829 | } |
830 | |
831 | The loaded class will be L<Moose>ified, so we are able to apply essential |
832 | roles. |
833 | |
b79aeec3 |
834 | =head2 Private Actions |
835 | |
836 | B<This option is a bit less, but still pretty experimental> |
837 | |
838 | You can declare private actions with the C<is private> trait: |
839 | |
840 | action end is private isa RenderView; |
841 | |
856ac9a7 |
842 | =head1 ROLES |
843 | |
844 | =over |
845 | |
846 | =item L<MooseX::Declare::Syntax::KeywordHandling> |
847 | |
848 | =back |
849 | |
850 | =head1 METHODS |
851 | |
852 | These methods are implementation details. Unless you are extending or |
853 | developing L<CatalystX::Declare>, you should not be concerned with them. |
854 | |
855 | =head2 parse |
856 | |
857 | Object->parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0) |
858 | |
859 | A hook that will be invoked by L<MooseX::Declare> when this instance is called |
860 | to handle syntax. It will parse the action declaration, prepare attributes and |
861 | add the actions to the controller. |
862 | |
863 | =head1 SEE ALSO |
864 | |
865 | =over |
866 | |
867 | =item L<CatalystX::Declare> |
868 | |
869 | =item L<CatalystX::Declare::Keyword::Controller> |
870 | |
871 | =item L<MooseX::Method::Signatures> |
872 | |
873 | =back |
874 | |
875 | =head1 AUTHOR |
876 | |
877 | See L<CatalystX::Declare/AUTHOR> for author information. |
878 | |
879 | =head1 LICENSE |
880 | |
881 | This program is free software; you can redistribute it and/or modify it under |
882 | the same terms as perl itself. |
918fb36e |
883 | |
856ac9a7 |
884 | =cut |
918fb36e |
885 | |