Commit | Line | Data |
918fb36e |
1 | use MooseX::Declare; |
2 | |
3 | class 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 | |