more and a bit better organized tests
[catagits/CatalystX-Declare.git] / lib / CatalystX / Declare / Keyword / Action.pm
CommitLineData
918fb36e 1use MooseX::Declare;
2
9c11a562 3class CatalystX::Declare::Keyword::Action
918fb36e 4 with MooseX::Declare::Syntax::KeywordHandling {
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 );
856ac9a7 11 use Moose::Util qw( add_method_modifier );
a1dd1788 12 use Class::Inspector;
13 use Class::MOP;
14
918fb36e 15
e10b92dd 16 use constant STOP_PARSING => '__MXDECLARE_STOP_PARSING__';
9c11a562 17 use constant UNDER_VAR => '$CatalystX::Declare::SCOPE::UNDER';
e10b92dd 18
918fb36e 19 use aliased 'MooseX::Method::Signatures::Meta::Method';
20 use aliased 'MooseX::MethodAttributes::Role::Meta::Method', 'AttributeRole';
21
22
856ac9a7 23 method parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0) {
918fb36e 24
25 # somewhere to put the attributes
26 my %attributes;
27 my @populators;
918fb36e 28
29 # parse declarations
30 until (do { $ctx->skipspace; $ctx->peek_next_char } eq any qw( ; { } )) {
918fb36e 31
32 $ctx->skipspace;
33
34 # optional commas
35 if ($ctx->peek_next_char eq ',') {
36
37 my $linestr = $ctx->get_linestr;
38 substr($linestr, $ctx->offset, 1) = '';
39 $ctx->set_linestr($linestr);
40
41 next;
42 }
43
44 # next thing should be an option name
45 my $option = (
46 $skipped_declarator
47 ? $ctx->strip_name
48 : do {
49 $ctx->skip_declarator;
50 $skipped_declarator++;
51 $ctx->declarator;
52 })
53 or croak "Expected option token, not " . substr($ctx->get_linestr, $ctx->offset);
54
55 # we need to be able to handle the rest
56 my $handler = $self->can("_handle_${option}_option")
57 or croak "Unknown action option: $option";
58
59 # call the handler
e10b92dd 60 my $populator = $self->$handler($ctx, \%attributes);
61
62 if ($populator and $populator eq STOP_PARSING) {
63
64 return $ctx->shadow(sub (&) {
65 my ($body) = @_;
66 return $body->();
67 });
68 }
69
70 push @populators, $populator
71 if defined $populator;
918fb36e 72 }
73
74 croak "Need an action specification"
75 unless exists $attributes{Signature};
76
77 my $name = $attributes{Subname};
856ac9a7 78
918fb36e 79 my $method = Method->wrap(
80 signature => qq{($attributes{Signature})},
81 package_name => $ctx->get_curstash_name,
82 name => $name,
83 );
84
a1dd1788 85 AttributeRole->meta->apply($method);
86
918fb36e 87 $_->($method)
88 for @populators;
89
aae7ad1f 90 unless ($attributes{Private}) {
91 $attributes{PathPart} ||= "'$name'";
918fb36e 92
aae7ad1f 93 delete $attributes{CaptureArgs}
94 if exists $attributes{Args};
918fb36e 95
aae7ad1f 96 $attributes{CaptureArgs} = 0
97 unless exists $attributes{Args}
98 or exists $attributes{CaptureArgs};
99 }
100
101 if ($attributes{Private}) {
aae7ad1f 102 delete $attributes{ $_ }
103 for qw( Args CaptureArgs Chained Signature Subname Action );
104 }
918fb36e 105
106 if ($ctx->peek_next_char eq '{') {
107 $ctx->inject_if_block($ctx->scope_injector_call . $method->injectable_code);
108 }
109 else {
110 $ctx->inject_code_parts_here(
111 sprintf '{ %s%s }',
112 $ctx->scope_injector_call,
113 $method->injectable_code,
114 );
115 }
116
918fb36e 117 my @attributes = map {
118 join('',
119 $_,
aae7ad1f 120 ref($attributes{ $_ }) eq 'ARRAY'
121 ? ( scalar(@{ $attributes{ $_ } })
122 ? sprintf('(%s)', join(' ', @{ $attributes{ $_ } }))
123 : '' )
124 : "($attributes{ $_ })"
918fb36e 125 );
126 } keys %attributes;
127
128 return $ctx->shadow(sub (&) {
129 my $class = caller;
856ac9a7 130 my $body = shift;
918fb36e 131
856ac9a7 132 $method->_set_actual_body($body);
918fb36e 133 $method->{attributes} = \@attributes;
856ac9a7 134
135 if ($modifier) {
136
137 add_method_modifier $class, $modifier, [$name, $method];
138 }
139 else {
140
141 $class->meta->add_method($name, $method);
142 $class->meta->register_method_attributes($class->can($method->name), \@attributes);
143 }
918fb36e 144 });
145 }
146
a1dd1788 147 method _handle_with_option (Object $ctx, HashRef $attrs) {
148
149 my $role = $ctx->strip_name
150 or croak "Expected bareword role specification for action after with";
151
152 # we need to fish for aliases here since we are still unclean
153 if (defined(my $alias = $self->_check_for_available_import($ctx, $role))) {
154 $role = $alias;
155 }
156
157 push @{ $attrs->{CatalystX_Declarative_ActionRoles} ||= [] }, $role;
158
159 return;
160 }
161
162 method _handle_isa_option (Object $ctx, HashRef $attrs) {
163
164 my $class = $ctx->strip_name
165 or croak "Expected bareword action class specification for action after isa";
166
167 if (defined(my $alias = $self->_check_for_available_import($ctx, $class))) {
168 $class = $alias;
169 }
170
171 $attrs->{CatalystX_Declarative_ActionClass} = $class;
172
173 return;
174 }
175
176 method _check_for_available_import (Object $ctx, Str $name) {
177
178 if (my $code = $ctx->get_curstash_name->can($name)) {
179 return $code->();
180 }
181
182 return undef;
183 }
184
918fb36e 185 method _handle_action_option (Object $ctx, HashRef $attrs) {
186
187 # action name
188 my $name = $ctx->strip_name
189 or croak "Anonymous actions not yet supported";
190
64baeca0 191 $ctx->skipspace;
192 my $populator;
193
194 if (substr($ctx->get_linestr, $ctx->offset, 2) eq '<-') {
195 my $linestr = $ctx->get_linestr;
196 substr($linestr, $ctx->offset, 2) = '';
197 $ctx->set_linestr($linestr);
198 $populator = $self->_handle_under_option($ctx, $attrs);
199 }
200
918fb36e 201 # signature
202 my $proto = $ctx->strip_proto || '';
203 $proto = join(', ', 'Object $self: Object $ctx', $proto || ());
204
205 $attrs->{Subname} = $name;
206 $attrs->{Signature} = $proto;
aae7ad1f 207 $attrs->{Action} = [];
918fb36e 208
9c11a562 209 if (defined $CatalystX::Declare::SCOPE::UNDER) {
210 $attrs->{Chained} ||= $CatalystX::Declare::SCOPE::UNDER;
e10b92dd 211 }
212
64baeca0 213 return unless $populator;
214 return $populator;
918fb36e 215 }
216
2dde75e7 217 method _handle_final_option (Object $ctx, HashRef $attrs) {
218
219 return $self->_build_flag_populator($ctx, $attrs, 'final');
220 }
221
918fb36e 222 method _handle_is_option (Object $ctx, HashRef $attrs) {
223
224 my $what = $ctx->strip_name
225 or croak "Expected symbol token after is symbol, not " . substr($ctx->get_linestr, $ctx->offset);
226
2dde75e7 227 return $self->_build_flag_populator($ctx, $attrs, $what);
228 }
229
230 method _build_flag_populator (Object $ctx, HashRef $attrs, Str $what) {
231
918fb36e 232 return sub {
233 my $method = shift;
234
235 if ($what eq any qw( end endpoint final )) {
236 my $count = $self->_count_positional_arguments($method);
237 $attrs->{Args} = defined($count) ? $count : '';
238 }
239 elsif ($what eq 'private') {
aae7ad1f 240 $attrs->{Private} = [];
918fb36e 241 }
242 };
243 }
244
245 method _handle_under_option (Object $ctx, HashRef $attrs) {
246
247 my $target = $self->_strip_actionpath($ctx);
e10b92dd 248 $ctx->skipspace;
249
250 if ($ctx->peek_next_char eq '{' and $self->identifier eq 'under') {
251 $ctx->inject_if_block(
a1dd1788 252 sprintf '%s; local %s; BEGIN { %s = qq(%s) };',
253 $ctx->scope_injector_call,
e10b92dd 254 UNDER_VAR,
255 UNDER_VAR,
256 $target,
257 );
258 return STOP_PARSING;
259 }
260
918fb36e 261 $attrs->{Chained} = "'$target'";
262
263 return sub {
264 my $method = shift;
265
266 my $count = $self->_count_positional_arguments($method);
267 $attrs->{CaptureArgs} = $count
268 if defined $count;
269 };
270 }
271
272 method _handle_chains_option (Object $ctx, HashRef $attrs) {
273
274 $ctx->skipspace;
275 $ctx->strip_name eq 'to'
276 or croak "Expected to token after chains symbol, not " . substr($ctx->get_linestr, $ctx->offset);
277
278 return $self->_handle_under_option($ctx, $attrs);
279 }
280
281 method _handle_as_option (Object $ctx, HashRef $attrs) {
282
283 $ctx->skipspace;
284
285 my $path = $self->_strip_actionpath($ctx);
286 $attrs->{PathPart} = "'$path'";
287
288 return;
289 }
290
291 method _count_positional_arguments (Object $method) {
292 my $signature = $method->_parsed_signature;
293
294 if ($signature->has_positional_params) {
295 my $count = @{ scalar($signature->positional_params) };
296
297 if ($count and ($signature->positional_params)[-1]->sigil eq '@') {
298 return undef;
299 }
300
301 return $count - 1;
302 }
303
304 return 0;
305 }
306
307 method _strip_actionpath (Object $ctx) {
308
309 $ctx->skipspace;
310 my $linestr = $ctx->get_linestr;
311 my $rest = substr($linestr, $ctx->offset);
312
313 if ($rest =~ /^ ( [_a-z] [_a-z0-9]* ) \b/ix) {
314 substr($linestr, $ctx->offset, length($1)) = '';
315 $ctx->set_linestr($linestr);
316 return $1;
317 }
a0ebba1d 318 elsif ($rest =~ /^ ' ( (?:[.:;,_a-z0-9]|\/)* ) ' /ix) {
918fb36e 319 substr($linestr, $ctx->offset, length($1) + 2) = '';
320 $ctx->set_linestr($linestr);
321 return $1;
322 }
323 else {
324 croak "Invalid syntax for action path: $rest";
325 }
326 }
327}
328
856ac9a7 329__END__
330
331=head1 NAME
332
333CatalystX::Declare::Keyword::Action - Declare Catalyst Actions
334
335=head1 SYNOPSIS
336
337 use CatalystX::Declare;
338
339 controller MyApp::Web::Controller::Example {
340
341 # chain base action with path part setting of ''
342 # body-less actions don't do anything by themselves
343 action base as '';
344
345 # simple end-point action
346 action controller_class is final under base {
347 $ctx->response->body( 'controller: ' . ref $self );
348 }
349
350 # chain part actions can have arguments
351 action str (Str $string) under base {
352
353 $ctx->stash(chars => [split //, $string]);
354 }
355
356 # and end point actions too, of course
357 action uc_chars (Int $count) under str is final {
358
359 my $chars = $ctx->stash->{chars};
360 ...
361 }
362
363
364 # you can use a shortcut for multiple actions with
365 # a common base
366 under base {
367
368 # this is an endpoint after base
369 action normal is final;
370
371 # the final keyword can be used to be more
372 # visually explicit about end-points
373 final action some_action { ... }
374 }
375
376 # of course you can also chain to external actions
377 final action some_end under '/some/controller/some/action';
378 }
379
380=head1 DESCRIPTION
381
382This handler class provides the user with C<action>, C<final> and C<under>
383keywords. There are multiple ways to define actions to allow for greater
384freedom of expression. While the parts of the action declaration itself do
385not care about their order, their syntax is rather strict.
386
387You can choose to separate syntax elements via C<,> if you think it is more
388readable. The action declaration
389
390 action foo is final under base;
391
392is parsed in exactly the same way if you write it as
393
394 action foo, is final, under base;
395
396=head2 Basic Action Declaration
397
398The simplest possible declaration is
399
400 action foo;
401
402This would define a chain-part action chained to C</> with the name C<foo>
403and no arguments. Since it isn't followed by a block, the body of the action
404will be empty.
405
406You will automatically be provided with two variables: C<$self> is, as you
407might expect, your controller instance. C<$ctx> will be the Catalyst context
408object. Thus, the following code would stash the value returned by the
409C<get_item> method:
410
411 action foo {
412 $ctx->stash(item => $self->get_item);
413 }
414
415=head2 Setting a Path Part
416
417As usual with Catalyst actions, the path part (the public name of this part of
418the URI, if you're not familiar with the term yet) will default to the name of
419the action itself (or more correctly: to whatever Catalyst defaults).
420
421To change that, use the C<as> option:
422
423 action base as ''; # <empty>
424 action something as 'foo/bar'; # foo/bar
425 action barely as bareword; # bareword
426
427=head2 Chaining Actions
428
429Currently, L<CatalystX::Declare> is completely based on the concept of
430L<chained actions|Catalyst::DispatchType::Chained>. Every action you declare is
431chained to something. No base specification means you chain to the root. You
432can specify the action you want to chain to with the C<under> option:
433
434 action foo; # chained to /
435 action foo under '/'; # also chained to /
436 action foo under bar; # chained to the local bar action
437 action foo under '/bar/baz'; # chained to baz in /bar
438
439C<under> is also provided as a grouping keyword. Every action inside the block
440will be chained to the specified action:
441
442 under base {
443 action foo { ... }
444 action bar { ... }
445 }
446
447You can also use the C<under> keyword for a single action. This is useful if
448you want to highlight a single action with a significant diversion from what
449is to be expected:
450
451 action base;
452
453 under '/the/sink' is final action foo;
454
455 final action bar under base;
456
457 final action baz under base;
458
459Instead of the C<under> option declaration, you can also use a more english
460variant named C<chains to>. While C<under> might be nice and concise, some
461people might prefer this if they confuse C<under> with the specification of
462a public path part. The argument to C<chains to> is the same as to C<under>:
463
464 action foo chains to bar;
465 action foo under bar;
466
467By default all actions are chain-parts, not end-points. If you want an action
468to be picked up as end-point and available via a public path, you have to say
469so explicitely by using the C<is final> option:
470
471 action base;
472 action foo under base is final; # /base/foo
473
474You can also drop the C<is> part of the C<is final> option if you want:
475
476 under base, final action foo { ... }
477
478You can make end-points more visually distinct by using the C<final> keyword
479instead of the option:
480
481 action base;
482 final action foo under base; # /base/foo
483
484And of course, the C<final>, C<under> and C<action> keywords can be used in
485combination whenever needed:
486
487 action base as '';
488
489 under base {
490
491 final action list; # /list
492
493 action load;
494
495 under load {
496
497 final action view; # /list/load/view
498 final action edit; # /list/load/edit
499 }
500 }
501
502There is also one shorthand alternative for declaring chain targets. You can
503specify an action after a C<E<lt>-> following the action name:
504
505 action base;
506 final action foo <- base; # /base/foo
507
508=head2 Arguments
509
510You can use signatures like you are use to from L<MooseX::Method::Signatures>
511to declare action parameters. The number of arguments will be used during
512dispatching. Dispatching by type constraint is planned but not yet implemented.
513
514The signature follows the action name:
515
516 # /foo/*/*/*
517 final action foo (Int $year, Int $month, Int $day);
518
519If you are using the shorthand definition, the signature follows the chain
520target:
521
522 # /foo/*
523 final action foo <- base ($x) { ... }
524
525Parameters may be specified on chain-parts and end-points:
526
527 # /base/*/foo/*
528 action base (Str $lang);
529 final action page (Int $page_num) under base;
530
531Named parameters will be populated with the values in the query parameters:
532
533 # /view/17/?page=3
534 final action view (Int $id, Int :$page = 1);
535
536Your end-points can also take an unspecified amount of arguments by specifying
537an array as a variable:
538
539 # /find/some/deep/path/spec
540 final action find (@path);
541
542=head2 Actions and Method Modifiers
543
544Method modifiers can not only be applied to methods, but also to actions. There
545is no way yet to override the attributes of an already established action via
546modifiers. However, you can modify the method underlying the action.
547
548The following code is an example role modifying the consuming controller's
549C<base> action:
550
551 use CatalystX::Declare;
552
553 controller_role MyApp::Web::ControllerRole::RichBase {
554
555 before base (Object $ctx) {
556 $ctx->stash(something => $ctx->model('Item'));
557 }
558 }
559
560Note that you have to specify the C<$ctx> argument yourself, since you are
561modifying a method, not an action.
562
563Any controller having a C<base> action (or method, for this purpose), can now
564consume the C<RichBase> role declared above:
565
566 use CatalystX::Declare;
567
568 controller MyApp::Web::Controller::Foo
569 with MyApp::Web::Controller::RichBase {
570
571 action base as '';
572
573 action show, final under base {
574 $ctx->response->body(
575 $ctx->stash->{something}->render,
576 );
577 }
578 }
579
580=head1 ROLES
581
582=over
583
584=item L<MooseX::Declare::Syntax::KeywordHandling>
585
586=back
587
588=head1 METHODS
589
590These methods are implementation details. Unless you are extending or
591developing L<CatalystX::Declare>, you should not be concerned with them.
592
593=head2 parse
594
595 Object->parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0)
596
597A hook that will be invoked by L<MooseX::Declare> when this instance is called
598to handle syntax. It will parse the action declaration, prepare attributes and
599add the actions to the controller.
600
601=head1 SEE ALSO
602
603=over
604
605=item L<CatalystX::Declare>
606
607=item L<CatalystX::Declare::Keyword::Controller>
608
609=item L<MooseX::Method::Signatures>
610
611=back
612
613=head1 AUTHOR
614
615See L<CatalystX::Declare/AUTHOR> for author information.
616
617=head1 LICENSE
618
619This program is free software; you can redistribute it and/or modify it under
620the same terms as perl itself.
918fb36e 621
856ac9a7 622=cut
918fb36e 623