added more configure_requires
[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
5fb5cef1 19 use aliased 'CatalystX::Declare::Action::CatchValidationError';
918fb36e 20 use aliased 'MooseX::Method::Signatures::Meta::Method';
21 use aliased 'MooseX::MethodAttributes::Role::Meta::Method', 'AttributeRole';
22
23
856ac9a7 24 method parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0) {
918fb36e 25
26 # somewhere to put the attributes
27 my %attributes;
28 my @populators;
918fb36e 29
30 # parse declarations
31 until (do { $ctx->skipspace; $ctx->peek_next_char } eq any qw( ; { } )) {
918fb36e 32
33 $ctx->skipspace;
34
35 # optional commas
36 if ($ctx->peek_next_char eq ',') {
37
38 my $linestr = $ctx->get_linestr;
39 substr($linestr, $ctx->offset, 1) = '';
40 $ctx->set_linestr($linestr);
41
42 next;
43 }
44
45 # next thing should be an option name
46 my $option = (
47 $skipped_declarator
48 ? $ctx->strip_name
49 : do {
50 $ctx->skip_declarator;
51 $skipped_declarator++;
52 $ctx->declarator;
53 })
54 or croak "Expected option token, not " . substr($ctx->get_linestr, $ctx->offset);
55
56 # we need to be able to handle the rest
57 my $handler = $self->can("_handle_${option}_option")
58 or croak "Unknown action option: $option";
59
60 # call the handler
e10b92dd 61 my $populator = $self->$handler($ctx, \%attributes);
62
63 if ($populator and $populator eq STOP_PARSING) {
64
65 return $ctx->shadow(sub (&) {
66 my ($body) = @_;
67 return $body->();
68 });
69 }
70
71 push @populators, $populator
72 if defined $populator;
918fb36e 73 }
74
75 croak "Need an action specification"
76 unless exists $attributes{Signature};
77
78 my $name = $attributes{Subname};
856ac9a7 79
918fb36e 80 my $method = Method->wrap(
81 signature => qq{($attributes{Signature})},
82 package_name => $ctx->get_curstash_name,
83 name => $name,
84 );
85
a1dd1788 86 AttributeRole->meta->apply($method);
87
eb97acbb 88 my $count = $self->_count_positional_arguments($method);
89 $attributes{CaptureArgs} = $count
90 if defined $count;
91
918fb36e 92 $_->($method)
93 for @populators;
94
aae7ad1f 95 unless ($attributes{Private}) {
96 $attributes{PathPart} ||= "'$name'";
918fb36e 97
aae7ad1f 98 delete $attributes{CaptureArgs}
99 if exists $attributes{Args};
918fb36e 100
aae7ad1f 101 $attributes{CaptureArgs} = 0
102 unless exists $attributes{Args}
103 or exists $attributes{CaptureArgs};
104 }
105
106 if ($attributes{Private}) {
aae7ad1f 107 delete $attributes{ $_ }
108 for qw( Args CaptureArgs Chained Signature Subname Action );
109 }
918fb36e 110
111 if ($ctx->peek_next_char eq '{') {
112 $ctx->inject_if_block($ctx->scope_injector_call . $method->injectable_code);
113 }
114 else {
115 $ctx->inject_code_parts_here(
116 sprintf '{ %s%s }',
117 $ctx->scope_injector_call,
118 $method->injectable_code,
119 );
120 }
121
5fb5cef1 122 my @attributes;
123 for my $attr (keys %attributes) {
124 push @attributes,
eb97acbb 125 map { sprintf '%s%s', $attr, defined($_) ? sprintf('(%s)', $_) : '' }
5fb5cef1 126 (ref($attributes{ $attr }) eq 'ARRAY')
127 ? @{ $attributes{ $attr } }
128 : $attributes{ $attr };
129 }
918fb36e 130
131 return $ctx->shadow(sub (&) {
132 my $class = caller;
856ac9a7 133 my $body = shift;
918fb36e 134
856ac9a7 135 $method->_set_actual_body($body);
918fb36e 136 $method->{attributes} = \@attributes;
856ac9a7 137
138 if ($modifier) {
139
140 add_method_modifier $class, $modifier, [$name, $method];
141 }
142 else {
143
144 $class->meta->add_method($name, $method);
145 $class->meta->register_method_attributes($class->can($method->name), \@attributes);
146 }
918fb36e 147 });
148 }
149
a1dd1788 150 method _handle_with_option (Object $ctx, HashRef $attrs) {
151
152 my $role = $ctx->strip_name
153 or croak "Expected bareword role specification for action after with";
154
155 # we need to fish for aliases here since we are still unclean
156 if (defined(my $alias = $self->_check_for_available_import($ctx, $role))) {
157 $role = $alias;
158 }
159
160 push @{ $attrs->{CatalystX_Declarative_ActionRoles} ||= [] }, $role;
161
162 return;
163 }
164
165 method _handle_isa_option (Object $ctx, HashRef $attrs) {
166
167 my $class = $ctx->strip_name
168 or croak "Expected bareword action class specification for action after isa";
169
170 if (defined(my $alias = $self->_check_for_available_import($ctx, $class))) {
171 $class = $alias;
172 }
173
174 $attrs->{CatalystX_Declarative_ActionClass} = $class;
175
176 return;
177 }
178
179 method _check_for_available_import (Object $ctx, Str $name) {
180
181 if (my $code = $ctx->get_curstash_name->can($name)) {
182 return $code->();
183 }
184
185 return undef;
186 }
187
918fb36e 188 method _handle_action_option (Object $ctx, HashRef $attrs) {
189
190 # action name
191 my $name = $ctx->strip_name
192 or croak "Anonymous actions not yet supported";
193
64baeca0 194 $ctx->skipspace;
195 my $populator;
196
197 if (substr($ctx->get_linestr, $ctx->offset, 2) eq '<-') {
198 my $linestr = $ctx->get_linestr;
199 substr($linestr, $ctx->offset, 2) = '';
200 $ctx->set_linestr($linestr);
201 $populator = $self->_handle_under_option($ctx, $attrs);
202 }
203
918fb36e 204 # signature
205 my $proto = $ctx->strip_proto || '';
206 $proto = join(', ', 'Object $self: Object $ctx', $proto || ());
207
208 $attrs->{Subname} = $name;
209 $attrs->{Signature} = $proto;
aae7ad1f 210 $attrs->{Action} = [];
918fb36e 211
5fb5cef1 212 push @{ $attrs->{CatalystX_Declarative_ActionRoles} ||= [] }, CatchValidationError;
213
9c11a562 214 if (defined $CatalystX::Declare::SCOPE::UNDER) {
215 $attrs->{Chained} ||= $CatalystX::Declare::SCOPE::UNDER;
e10b92dd 216 }
217
64baeca0 218 return unless $populator;
219 return $populator;
918fb36e 220 }
221
2dde75e7 222 method _handle_final_option (Object $ctx, HashRef $attrs) {
223
224 return $self->_build_flag_populator($ctx, $attrs, 'final');
225 }
226
918fb36e 227 method _handle_is_option (Object $ctx, HashRef $attrs) {
228
229 my $what = $ctx->strip_name
230 or croak "Expected symbol token after is symbol, not " . substr($ctx->get_linestr, $ctx->offset);
231
2dde75e7 232 return $self->_build_flag_populator($ctx, $attrs, $what);
233 }
234
235 method _build_flag_populator (Object $ctx, HashRef $attrs, Str $what) {
236
918fb36e 237 return sub {
238 my $method = shift;
239
240 if ($what eq any qw( end endpoint final )) {
eb97acbb 241 $attrs->{Args} = delete $attrs->{CaptureArgs};
918fb36e 242 }
243 elsif ($what eq 'private') {
aae7ad1f 244 $attrs->{Private} = [];
918fb36e 245 }
246 };
247 }
248
249 method _handle_under_option (Object $ctx, HashRef $attrs) {
250
251 my $target = $self->_strip_actionpath($ctx);
e10b92dd 252 $ctx->skipspace;
253
254 if ($ctx->peek_next_char eq '{' and $self->identifier eq 'under') {
255 $ctx->inject_if_block(
a1dd1788 256 sprintf '%s; local %s; BEGIN { %s = qq(%s) };',
257 $ctx->scope_injector_call,
e10b92dd 258 UNDER_VAR,
259 UNDER_VAR,
260 $target,
261 );
262 return STOP_PARSING;
263 }
264
918fb36e 265 $attrs->{Chained} = "'$target'";
266
267 return sub {
268 my $method = shift;
918fb36e 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
6e2492a4 343 action base as '' under '/';
856ac9a7 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
6e2492a4 402This would define a chain-part action chained to nothing with the name C<foo>
856ac9a7 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
6e2492a4 423 under something {
424 action base as ''; # <empty>
425 action something as 'foo/bar'; # foo/bar
426 action barely as bareword; # bareword
427 }
856ac9a7 428
429=head2 Chaining Actions
430
431Currently, L<CatalystX::Declare> is completely based on the concept of
432L<chained actions|Catalyst::DispatchType::Chained>. Every action you declare is
6e2492a4 433chained or private. You can specify the action you want to chain to with the
434C<under> option:
856ac9a7 435
6e2492a4 436 action foo; # chained to nothing
856ac9a7 437 action foo under '/'; # also chained to /
438 action foo under bar; # chained to the local bar action
439 action foo under '/bar/baz'; # chained to baz in /bar
440
441C<under> is also provided as a grouping keyword. Every action inside the block
442will be chained to the specified action:
443
444 under base {
445 action foo { ... }
446 action bar { ... }
447 }
448
449You can also use the C<under> keyword for a single action. This is useful if
450you want to highlight a single action with a significant diversion from what
451is to be expected:
452
6e2492a4 453 action base under '/';
856ac9a7 454
455 under '/the/sink' is final action foo;
456
457 final action bar under base;
458
459 final action baz under base;
460
461Instead of the C<under> option declaration, you can also use a more english
462variant named C<chains to>. While C<under> might be nice and concise, some
463people might prefer this if they confuse C<under> with the specification of
464a public path part. The argument to C<chains to> is the same as to C<under>:
465
466 action foo chains to bar;
467 action foo under bar;
468
469By default all actions are chain-parts, not end-points. If you want an action
470to be picked up as end-point and available via a public path, you have to say
471so explicitely by using the C<is final> option:
472
6e2492a4 473 action base under '/';
856ac9a7 474 action foo under base is final; # /base/foo
475
476You can also drop the C<is> part of the C<is final> option if you want:
477
478 under base, final action foo { ... }
479
480You can make end-points more visually distinct by using the C<final> keyword
481instead of the option:
482
6e2492a4 483 action base under '/';
856ac9a7 484 final action foo under base; # /base/foo
485
486And of course, the C<final>, C<under> and C<action> keywords can be used in
487combination whenever needed:
488
6e2492a4 489 action base as '' under '/';
856ac9a7 490
491 under base {
492
493 final action list; # /list
494
495 action load;
496
497 under load {
498
499 final action view; # /list/load/view
500 final action edit; # /list/load/edit
501 }
502 }
503
504There is also one shorthand alternative for declaring chain targets. You can
505specify an action after a C<E<lt>-> following the action name:
506
6e2492a4 507 action base under '/';
856ac9a7 508 final action foo <- base; # /base/foo
509
510=head2 Arguments
511
512You can use signatures like you are use to from L<MooseX::Method::Signatures>
513to declare action parameters. The number of arguments will be used during
514dispatching. Dispatching by type constraint is planned but not yet implemented.
515
516The signature follows the action name:
517
518 # /foo/*/*/*
519 final action foo (Int $year, Int $month, Int $day);
520
521If you are using the shorthand definition, the signature follows the chain
522target:
523
524 # /foo/*
6e2492a4 525 final action foo <- base ($x) under '/' { ... }
856ac9a7 526
527Parameters may be specified on chain-parts and end-points:
528
529 # /base/*/foo/*
6e2492a4 530 action base (Str $lang) under '/';
856ac9a7 531 final action page (Int $page_num) under base;
532
533Named parameters will be populated with the values in the query parameters:
534
535 # /view/17/?page=3
6e2492a4 536 final action view (Int $id, Int :$page = 1) under '/';
856ac9a7 537
538Your end-points can also take an unspecified amount of arguments by specifying
539an array as a variable:
540
541 # /find/some/deep/path/spec
6e2492a4 542 final action find (@path) under '/';
856ac9a7 543
5fb5cef1 544=head2 Validation
545
546Currently, when the arguments do not fit the signature because of a L<Moose>
547validation error, the response body will be set to C<Bad Request> and the
392e5076 548status to C<400>. This only applies when debug mode is off. If it is turned on,
549the error message will be prefixed with C<BAD REQUEST: >.
5fb5cef1 550
856ac9a7 551=head2 Actions and Method Modifiers
552
553Method modifiers can not only be applied to methods, but also to actions. There
554is no way yet to override the attributes of an already established action via
555modifiers. However, you can modify the method underlying the action.
556
557The following code is an example role modifying the consuming controller's
558C<base> action:
559
560 use CatalystX::Declare;
561
e2dc918e 562 component_role MyApp::Web::ControllerRole::RichBase {
856ac9a7 563
564 before base (Object $ctx) {
565 $ctx->stash(something => $ctx->model('Item'));
566 }
567 }
568
569Note that you have to specify the C<$ctx> argument yourself, since you are
570modifying a method, not an action.
571
572Any controller having a C<base> action (or method, for this purpose), can now
573consume the C<RichBase> role declared above:
574
575 use CatalystX::Declare;
576
577 controller MyApp::Web::Controller::Foo
578 with MyApp::Web::Controller::RichBase {
579
6e2492a4 580 action base as '' under '/';
856ac9a7 581
582 action show, final under base {
583 $ctx->response->body(
584 $ctx->stash->{something}->render,
585 );
586 }
587 }
588
589=head1 ROLES
590
591=over
592
593=item L<MooseX::Declare::Syntax::KeywordHandling>
594
595=back
596
597=head1 METHODS
598
599These methods are implementation details. Unless you are extending or
600developing L<CatalystX::Declare>, you should not be concerned with them.
601
602=head2 parse
603
604 Object->parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0)
605
606A hook that will be invoked by L<MooseX::Declare> when this instance is called
607to handle syntax. It will parse the action declaration, prepare attributes and
608add the actions to the controller.
609
610=head1 SEE ALSO
611
612=over
613
614=item L<CatalystX::Declare>
615
616=item L<CatalystX::Declare::Keyword::Controller>
617
618=item L<MooseX::Method::Signatures>
619
620=back
621
622=head1 AUTHOR
623
624See L<CatalystX::Declare/AUTHOR> for author information.
625
626=head1 LICENSE
627
628This program is free software; you can redistribute it and/or modify it under
629the same terms as perl itself.
918fb36e 630
856ac9a7 631=cut
918fb36e 632