allow under specification inside action syntax via <-
[catagits/CatalystX-Declare.git] / lib / CatalystX / Declarative / Keyword / Action.pm
CommitLineData
918fb36e 1use MooseX::Declare;
2
3class CatalystX::Declarative::Keyword::Action
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 );
11 use Class::Inspector;
12 use Class::MOP;
13
918fb36e 14
e10b92dd 15 use constant STOP_PARSING => '__MXDECLARE_STOP_PARSING__';
16 use constant UNDER_VAR => '$CatalystX::Declarative::SCOPE::UNDER';
17
918fb36e 18 use aliased 'MooseX::Method::Signatures::Meta::Method';
19 use aliased 'MooseX::MethodAttributes::Role::Meta::Method', 'AttributeRole';
20
21
22 method parse (Object $ctx) {
23
24 # somewhere to put the attributes
25 my %attributes;
26 my @populators;
27 my $skipped_declarator;
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};
78 my $method = Method->wrap(
79 signature => qq{($attributes{Signature})},
80 package_name => $ctx->get_curstash_name,
81 name => $name,
82 );
83
a1dd1788 84 AttributeRole->meta->apply($method);
85
918fb36e 86 $_->($method)
87 for @populators;
88
aae7ad1f 89 unless ($attributes{Private}) {
90 $attributes{PathPart} ||= "'$name'";
918fb36e 91
aae7ad1f 92 delete $attributes{CaptureArgs}
93 if exists $attributes{Args};
918fb36e 94
aae7ad1f 95 $attributes{CaptureArgs} = 0
96 unless exists $attributes{Args}
97 or exists $attributes{CaptureArgs};
98 }
99
100 if ($attributes{Private}) {
aae7ad1f 101 delete $attributes{ $_ }
102 for qw( Args CaptureArgs Chained Signature Subname Action );
103 }
918fb36e 104
105 if ($ctx->peek_next_char eq '{') {
106 $ctx->inject_if_block($ctx->scope_injector_call . $method->injectable_code);
107 }
108 else {
109 $ctx->inject_code_parts_here(
110 sprintf '{ %s%s }',
111 $ctx->scope_injector_call,
112 $method->injectable_code,
113 );
114 }
115
918fb36e 116 my @attributes = map {
117 join('',
118 $_,
aae7ad1f 119 ref($attributes{ $_ }) eq 'ARRAY'
120 ? ( scalar(@{ $attributes{ $_ } })
121 ? sprintf('(%s)', join(' ', @{ $attributes{ $_ } }))
122 : '' )
123 : "($attributes{ $_ })"
918fb36e 124 );
125 } keys %attributes;
126
127 return $ctx->shadow(sub (&) {
128 my $class = caller;
129
130 $method->_set_actual_body(shift);
131 $method->{attributes} = \@attributes;
132
133 $class->meta->add_method($name, $method);
e10b92dd 134 $class->meta->register_method_attributes($class->can($method->name), \@attributes);
918fb36e 135 });
136 }
137
a1dd1788 138 method _handle_with_option (Object $ctx, HashRef $attrs) {
139
140 my $role = $ctx->strip_name
141 or croak "Expected bareword role specification for action after with";
142
143 # we need to fish for aliases here since we are still unclean
144 if (defined(my $alias = $self->_check_for_available_import($ctx, $role))) {
145 $role = $alias;
146 }
147
148 push @{ $attrs->{CatalystX_Declarative_ActionRoles} ||= [] }, $role;
149
150 return;
151 }
152
153 method _handle_isa_option (Object $ctx, HashRef $attrs) {
154
155 my $class = $ctx->strip_name
156 or croak "Expected bareword action class specification for action after isa";
157
158 if (defined(my $alias = $self->_check_for_available_import($ctx, $class))) {
159 $class = $alias;
160 }
161
162 $attrs->{CatalystX_Declarative_ActionClass} = $class;
163
164 return;
165 }
166
167 method _check_for_available_import (Object $ctx, Str $name) {
168
169 if (my $code = $ctx->get_curstash_name->can($name)) {
170 return $code->();
171 }
172
173 return undef;
174 }
175
918fb36e 176 method _handle_action_option (Object $ctx, HashRef $attrs) {
177
178 # action name
179 my $name = $ctx->strip_name
180 or croak "Anonymous actions not yet supported";
181
64baeca0 182 $ctx->skipspace;
183 my $populator;
184
185 if (substr($ctx->get_linestr, $ctx->offset, 2) eq '<-') {
186 my $linestr = $ctx->get_linestr;
187 substr($linestr, $ctx->offset, 2) = '';
188 $ctx->set_linestr($linestr);
189 $populator = $self->_handle_under_option($ctx, $attrs);
190 }
191
918fb36e 192 # signature
193 my $proto = $ctx->strip_proto || '';
194 $proto = join(', ', 'Object $self: Object $ctx', $proto || ());
195
196 $attrs->{Subname} = $name;
197 $attrs->{Signature} = $proto;
aae7ad1f 198 $attrs->{Action} = [];
918fb36e 199
e10b92dd 200 if (defined $CatalystX::Declarative::SCOPE::UNDER) {
201 $attrs->{Chained} ||= $CatalystX::Declarative::SCOPE::UNDER;
202 }
203
64baeca0 204 return unless $populator;
205 return $populator;
918fb36e 206 }
207
2dde75e7 208 method _handle_final_option (Object $ctx, HashRef $attrs) {
209
210 return $self->_build_flag_populator($ctx, $attrs, 'final');
211 }
212
918fb36e 213 method _handle_is_option (Object $ctx, HashRef $attrs) {
214
215 my $what = $ctx->strip_name
216 or croak "Expected symbol token after is symbol, not " . substr($ctx->get_linestr, $ctx->offset);
217
2dde75e7 218 return $self->_build_flag_populator($ctx, $attrs, $what);
219 }
220
221 method _build_flag_populator (Object $ctx, HashRef $attrs, Str $what) {
222
918fb36e 223 return sub {
224 my $method = shift;
225
226 if ($what eq any qw( end endpoint final )) {
227 my $count = $self->_count_positional_arguments($method);
228 $attrs->{Args} = defined($count) ? $count : '';
229 }
230 elsif ($what eq 'private') {
aae7ad1f 231 $attrs->{Private} = [];
918fb36e 232 }
233 };
234 }
235
236 method _handle_under_option (Object $ctx, HashRef $attrs) {
237
238 my $target = $self->_strip_actionpath($ctx);
e10b92dd 239 $ctx->skipspace;
240
241 if ($ctx->peek_next_char eq '{' and $self->identifier eq 'under') {
242 $ctx->inject_if_block(
a1dd1788 243 sprintf '%s; local %s; BEGIN { %s = qq(%s) };',
244 $ctx->scope_injector_call,
e10b92dd 245 UNDER_VAR,
246 UNDER_VAR,
247 $target,
248 );
249 return STOP_PARSING;
250 }
251
918fb36e 252 $attrs->{Chained} = "'$target'";
253
254 return sub {
255 my $method = shift;
256
257 my $count = $self->_count_positional_arguments($method);
258 $attrs->{CaptureArgs} = $count
259 if defined $count;
260 };
261 }
262
263 method _handle_chains_option (Object $ctx, HashRef $attrs) {
264
265 $ctx->skipspace;
266 $ctx->strip_name eq 'to'
267 or croak "Expected to token after chains symbol, not " . substr($ctx->get_linestr, $ctx->offset);
268
269 return $self->_handle_under_option($ctx, $attrs);
270 }
271
272 method _handle_as_option (Object $ctx, HashRef $attrs) {
273
274 $ctx->skipspace;
275
276 my $path = $self->_strip_actionpath($ctx);
277 $attrs->{PathPart} = "'$path'";
278
279 return;
280 }
281
282 method _count_positional_arguments (Object $method) {
283 my $signature = $method->_parsed_signature;
284
285 if ($signature->has_positional_params) {
286 my $count = @{ scalar($signature->positional_params) };
287
288 if ($count and ($signature->positional_params)[-1]->sigil eq '@') {
289 return undef;
290 }
291
292 return $count - 1;
293 }
294
295 return 0;
296 }
297
298 method _strip_actionpath (Object $ctx) {
299
300 $ctx->skipspace;
301 my $linestr = $ctx->get_linestr;
302 my $rest = substr($linestr, $ctx->offset);
303
304 if ($rest =~ /^ ( [_a-z] [_a-z0-9]* ) \b/ix) {
305 substr($linestr, $ctx->offset, length($1)) = '';
306 $ctx->set_linestr($linestr);
307 return $1;
308 }
a0ebba1d 309 elsif ($rest =~ /^ ' ( (?:[.:;,_a-z0-9]|\/)* ) ' /ix) {
918fb36e 310 substr($linestr, $ctx->offset, length($1) + 2) = '';
311 $ctx->set_linestr($linestr);
312 return $1;
313 }
314 else {
315 croak "Invalid syntax for action path: $rest";
316 }
317 }
318}
319
320
321