validation error now leads to 400 bad request
[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
918fb36e 88 $_->($method)
89 for @populators;
90
aae7ad1f 91 unless ($attributes{Private}) {
92 $attributes{PathPart} ||= "'$name'";
918fb36e 93
aae7ad1f 94 delete $attributes{CaptureArgs}
95 if exists $attributes{Args};
918fb36e 96
aae7ad1f 97 $attributes{CaptureArgs} = 0
98 unless exists $attributes{Args}
99 or exists $attributes{CaptureArgs};
100 }
101
102 if ($attributes{Private}) {
aae7ad1f 103 delete $attributes{ $_ }
104 for qw( Args CaptureArgs Chained Signature Subname Action );
105 }
918fb36e 106
107 if ($ctx->peek_next_char eq '{') {
108 $ctx->inject_if_block($ctx->scope_injector_call . $method->injectable_code);
109 }
110 else {
111 $ctx->inject_code_parts_here(
112 sprintf '{ %s%s }',
113 $ctx->scope_injector_call,
114 $method->injectable_code,
115 );
116 }
117
5fb5cef1 118 my @attributes;
119 for my $attr (keys %attributes) {
120 push @attributes,
121 map { sprintf '%s(%s)', $attr, $_ }
122 (ref($attributes{ $attr }) eq 'ARRAY')
123 ? @{ $attributes{ $attr } }
124 : $attributes{ $attr };
125 }
918fb36e 126
127 return $ctx->shadow(sub (&) {
128 my $class = caller;
856ac9a7 129 my $body = shift;
918fb36e 130
856ac9a7 131 $method->_set_actual_body($body);
918fb36e 132 $method->{attributes} = \@attributes;
856ac9a7 133
134 if ($modifier) {
135
136 add_method_modifier $class, $modifier, [$name, $method];
137 }
138 else {
139
140 $class->meta->add_method($name, $method);
141 $class->meta->register_method_attributes($class->can($method->name), \@attributes);
142 }
918fb36e 143 });
144 }
145
a1dd1788 146 method _handle_with_option (Object $ctx, HashRef $attrs) {
147
148 my $role = $ctx->strip_name
149 or croak "Expected bareword role specification for action after with";
150
151 # we need to fish for aliases here since we are still unclean
152 if (defined(my $alias = $self->_check_for_available_import($ctx, $role))) {
153 $role = $alias;
154 }
155
156 push @{ $attrs->{CatalystX_Declarative_ActionRoles} ||= [] }, $role;
157
158 return;
159 }
160
161 method _handle_isa_option (Object $ctx, HashRef $attrs) {
162
163 my $class = $ctx->strip_name
164 or croak "Expected bareword action class specification for action after isa";
165
166 if (defined(my $alias = $self->_check_for_available_import($ctx, $class))) {
167 $class = $alias;
168 }
169
170 $attrs->{CatalystX_Declarative_ActionClass} = $class;
171
172 return;
173 }
174
175 method _check_for_available_import (Object $ctx, Str $name) {
176
177 if (my $code = $ctx->get_curstash_name->can($name)) {
178 return $code->();
179 }
180
181 return undef;
182 }
183
918fb36e 184 method _handle_action_option (Object $ctx, HashRef $attrs) {
185
186 # action name
187 my $name = $ctx->strip_name
188 or croak "Anonymous actions not yet supported";
189
64baeca0 190 $ctx->skipspace;
191 my $populator;
192
193 if (substr($ctx->get_linestr, $ctx->offset, 2) eq '<-') {
194 my $linestr = $ctx->get_linestr;
195 substr($linestr, $ctx->offset, 2) = '';
196 $ctx->set_linestr($linestr);
197 $populator = $self->_handle_under_option($ctx, $attrs);
198 }
199
918fb36e 200 # signature
201 my $proto = $ctx->strip_proto || '';
202 $proto = join(', ', 'Object $self: Object $ctx', $proto || ());
203
204 $attrs->{Subname} = $name;
205 $attrs->{Signature} = $proto;
aae7ad1f 206 $attrs->{Action} = [];
918fb36e 207
5fb5cef1 208 push @{ $attrs->{CatalystX_Declarative_ActionRoles} ||= [] }, CatchValidationError;
209
9c11a562 210 if (defined $CatalystX::Declare::SCOPE::UNDER) {
211 $attrs->{Chained} ||= $CatalystX::Declare::SCOPE::UNDER;
e10b92dd 212 }
213
64baeca0 214 return unless $populator;
215 return $populator;
918fb36e 216 }
217
2dde75e7 218 method _handle_final_option (Object $ctx, HashRef $attrs) {
219
220 return $self->_build_flag_populator($ctx, $attrs, 'final');
221 }
222
918fb36e 223 method _handle_is_option (Object $ctx, HashRef $attrs) {
224
225 my $what = $ctx->strip_name
226 or croak "Expected symbol token after is symbol, not " . substr($ctx->get_linestr, $ctx->offset);
227
2dde75e7 228 return $self->_build_flag_populator($ctx, $attrs, $what);
229 }
230
231 method _build_flag_populator (Object $ctx, HashRef $attrs, Str $what) {
232
918fb36e 233 return sub {
234 my $method = shift;
235
236 if ($what eq any qw( end endpoint final )) {
237 my $count = $self->_count_positional_arguments($method);
238 $attrs->{Args} = defined($count) ? $count : '';
239 }
240 elsif ($what eq 'private') {
aae7ad1f 241 $attrs->{Private} = [];
918fb36e 242 }
243 };
244 }
245
246 method _handle_under_option (Object $ctx, HashRef $attrs) {
247
248 my $target = $self->_strip_actionpath($ctx);
e10b92dd 249 $ctx->skipspace;
250
251 if ($ctx->peek_next_char eq '{' and $self->identifier eq 'under') {
252 $ctx->inject_if_block(
a1dd1788 253 sprintf '%s; local %s; BEGIN { %s = qq(%s) };',
254 $ctx->scope_injector_call,
e10b92dd 255 UNDER_VAR,
256 UNDER_VAR,
257 $target,
258 );
259 return STOP_PARSING;
260 }
261
918fb36e 262 $attrs->{Chained} = "'$target'";
263
264 return sub {
265 my $method = shift;
266
267 my $count = $self->_count_positional_arguments($method);
268 $attrs->{CaptureArgs} = $count
269 if defined $count;
270 };
271 }
272
273 method _handle_chains_option (Object $ctx, HashRef $attrs) {
274
275 $ctx->skipspace;
276 $ctx->strip_name eq 'to'
277 or croak "Expected to token after chains symbol, not " . substr($ctx->get_linestr, $ctx->offset);
278
279 return $self->_handle_under_option($ctx, $attrs);
280 }
281
282 method _handle_as_option (Object $ctx, HashRef $attrs) {
283
284 $ctx->skipspace;
285
286 my $path = $self->_strip_actionpath($ctx);
287 $attrs->{PathPart} = "'$path'";
288
289 return;
290 }
291
292 method _count_positional_arguments (Object $method) {
293 my $signature = $method->_parsed_signature;
294
295 if ($signature->has_positional_params) {
296 my $count = @{ scalar($signature->positional_params) };
297
298 if ($count and ($signature->positional_params)[-1]->sigil eq '@') {
299 return undef;
300 }
301
302 return $count - 1;
303 }
304
305 return 0;
306 }
307
308 method _strip_actionpath (Object $ctx) {
309
310 $ctx->skipspace;
311 my $linestr = $ctx->get_linestr;
312 my $rest = substr($linestr, $ctx->offset);
313
314 if ($rest =~ /^ ( [_a-z] [_a-z0-9]* ) \b/ix) {
315 substr($linestr, $ctx->offset, length($1)) = '';
316 $ctx->set_linestr($linestr);
317 return $1;
318 }
a0ebba1d 319 elsif ($rest =~ /^ ' ( (?:[.:;,_a-z0-9]|\/)* ) ' /ix) {
918fb36e 320 substr($linestr, $ctx->offset, length($1) + 2) = '';
321 $ctx->set_linestr($linestr);
322 return $1;
323 }
324 else {
325 croak "Invalid syntax for action path: $rest";
326 }
327 }
328}
329
856ac9a7 330__END__
331
332=head1 NAME
333
334CatalystX::Declare::Keyword::Action - Declare Catalyst Actions
335
336=head1 SYNOPSIS
337
338 use CatalystX::Declare;
339
340 controller MyApp::Web::Controller::Example {
341
342 # chain base action with path part setting of ''
343 # body-less actions don't do anything by themselves
6e2492a4 344 action base as '' under '/';
856ac9a7 345
346 # simple end-point action
347 action controller_class is final under base {
348 $ctx->response->body( 'controller: ' . ref $self );
349 }
350
351 # chain part actions can have arguments
352 action str (Str $string) under base {
353
354 $ctx->stash(chars => [split //, $string]);
355 }
356
357 # and end point actions too, of course
358 action uc_chars (Int $count) under str is final {
359
360 my $chars = $ctx->stash->{chars};
361 ...
362 }
363
364
365 # you can use a shortcut for multiple actions with
366 # a common base
367 under base {
368
369 # this is an endpoint after base
370 action normal is final;
371
372 # the final keyword can be used to be more
373 # visually explicit about end-points
374 final action some_action { ... }
375 }
376
377 # of course you can also chain to external actions
378 final action some_end under '/some/controller/some/action';
379 }
380
381=head1 DESCRIPTION
382
383This handler class provides the user with C<action>, C<final> and C<under>
384keywords. There are multiple ways to define actions to allow for greater
385freedom of expression. While the parts of the action declaration itself do
386not care about their order, their syntax is rather strict.
387
388You can choose to separate syntax elements via C<,> if you think it is more
389readable. The action declaration
390
391 action foo is final under base;
392
393is parsed in exactly the same way if you write it as
394
395 action foo, is final, under base;
396
397=head2 Basic Action Declaration
398
399The simplest possible declaration is
400
401 action foo;
402
6e2492a4 403This would define a chain-part action chained to nothing with the name C<foo>
856ac9a7 404and no arguments. Since it isn't followed by a block, the body of the action
405will be empty.
406
407You will automatically be provided with two variables: C<$self> is, as you
408might expect, your controller instance. C<$ctx> will be the Catalyst context
409object. Thus, the following code would stash the value returned by the
410C<get_item> method:
411
412 action foo {
413 $ctx->stash(item => $self->get_item);
414 }
415
416=head2 Setting a Path Part
417
418As usual with Catalyst actions, the path part (the public name of this part of
419the URI, if you're not familiar with the term yet) will default to the name of
420the action itself (or more correctly: to whatever Catalyst defaults).
421
422To change that, use the C<as> option:
423
6e2492a4 424 under something {
425 action base as ''; # <empty>
426 action something as 'foo/bar'; # foo/bar
427 action barely as bareword; # bareword
428 }
856ac9a7 429
430=head2 Chaining Actions
431
432Currently, L<CatalystX::Declare> is completely based on the concept of
433L<chained actions|Catalyst::DispatchType::Chained>. Every action you declare is
6e2492a4 434chained or private. You can specify the action you want to chain to with the
435C<under> option:
856ac9a7 436
6e2492a4 437 action foo; # chained to nothing
856ac9a7 438 action foo under '/'; # also chained to /
439 action foo under bar; # chained to the local bar action
440 action foo under '/bar/baz'; # chained to baz in /bar
441
442C<under> is also provided as a grouping keyword. Every action inside the block
443will be chained to the specified action:
444
445 under base {
446 action foo { ... }
447 action bar { ... }
448 }
449
450You can also use the C<under> keyword for a single action. This is useful if
451you want to highlight a single action with a significant diversion from what
452is to be expected:
453
6e2492a4 454 action base under '/';
856ac9a7 455
456 under '/the/sink' is final action foo;
457
458 final action bar under base;
459
460 final action baz under base;
461
462Instead of the C<under> option declaration, you can also use a more english
463variant named C<chains to>. While C<under> might be nice and concise, some
464people might prefer this if they confuse C<under> with the specification of
465a public path part. The argument to C<chains to> is the same as to C<under>:
466
467 action foo chains to bar;
468 action foo under bar;
469
470By default all actions are chain-parts, not end-points. If you want an action
471to be picked up as end-point and available via a public path, you have to say
472so explicitely by using the C<is final> option:
473
6e2492a4 474 action base under '/';
856ac9a7 475 action foo under base is final; # /base/foo
476
477You can also drop the C<is> part of the C<is final> option if you want:
478
479 under base, final action foo { ... }
480
481You can make end-points more visually distinct by using the C<final> keyword
482instead of the option:
483
6e2492a4 484 action base under '/';
856ac9a7 485 final action foo under base; # /base/foo
486
487And of course, the C<final>, C<under> and C<action> keywords can be used in
488combination whenever needed:
489
6e2492a4 490 action base as '' under '/';
856ac9a7 491
492 under base {
493
494 final action list; # /list
495
496 action load;
497
498 under load {
499
500 final action view; # /list/load/view
501 final action edit; # /list/load/edit
502 }
503 }
504
505There is also one shorthand alternative for declaring chain targets. You can
506specify an action after a C<E<lt>-> following the action name:
507
6e2492a4 508 action base under '/';
856ac9a7 509 final action foo <- base; # /base/foo
510
511=head2 Arguments
512
513You can use signatures like you are use to from L<MooseX::Method::Signatures>
514to declare action parameters. The number of arguments will be used during
515dispatching. Dispatching by type constraint is planned but not yet implemented.
516
517The signature follows the action name:
518
519 # /foo/*/*/*
520 final action foo (Int $year, Int $month, Int $day);
521
522If you are using the shorthand definition, the signature follows the chain
523target:
524
525 # /foo/*
6e2492a4 526 final action foo <- base ($x) under '/' { ... }
856ac9a7 527
528Parameters may be specified on chain-parts and end-points:
529
530 # /base/*/foo/*
6e2492a4 531 action base (Str $lang) under '/';
856ac9a7 532 final action page (Int $page_num) under base;
533
534Named parameters will be populated with the values in the query parameters:
535
536 # /view/17/?page=3
6e2492a4 537 final action view (Int $id, Int :$page = 1) under '/';
856ac9a7 538
539Your end-points can also take an unspecified amount of arguments by specifying
540an array as a variable:
541
542 # /find/some/deep/path/spec
6e2492a4 543 final action find (@path) under '/';
856ac9a7 544
5fb5cef1 545=head2 Validation
546
547Currently, when the arguments do not fit the signature because of a L<Moose>
548validation error, the response body will be set to C<Bad Request> and the
549status to C<400>.
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
562 controller_role MyApp::Web::ControllerRole::RichBase {
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