File Coverage

blib/lib/Class/StateMachine/Declarative/Builder.pm
Criterion Covered Total %
statement 85 260 32.6
branch 11 120 9.1
condition 6 78 7.6
subroutine 15 40 37.5
pod 0 3 0.0
total 117 501 23.3


line stmt bran cond sub pod time code
1             package Class::StateMachine::Declarative::Builder;
2              
3 1     1   5 use strict;
  1         2  
  1         37  
4 1     1   6 use warnings;
  1         2  
  1         27  
5 1     1   7 use Carp;
  1         1  
  1         64  
6 1     1   19 use 5.010;
  1         4  
  1         52  
7 1     1   6 use Scalar::Util ();
  1         1  
  1         27  
8              
9 1     1   1206 use Class::StateMachine;
  1         29663  
  1         7  
10             *debug = \$Class::StateMachine::debug;
11             *_debug = \&Class::StateMachine::Private::_debug;
12             our $debug;
13              
14             sub new {
15 1     1 0 3 my ($class, $target_class) = @_;
16 1         8 my $top = Class::StateMachine::Declarative::Builder::State->_new;
17 1         5 my $self = { top => $top,
18             states => { '/' => $top },
19             class => $target_class };
20 1         2 bless $self, $class;
21 1         4 $self;
22             }
23              
24             sub _bad_def {
25 0     0   0 my ($self, $state, @msg) = @_;
26 0         0 croak "@msg on definition of state '$state->{name}' for class '$self->{class}'";
27             }
28              
29 0     0   0 sub _is_hash { UNIVERSAL::isa($_[0], 'HASH') }
30 0     0   0 sub _is_array { UNIVERSAL::isa($_[0], 'ARRAY') }
31              
32             sub _ensure_list {
33 0     0   0 my $ref = shift;
34 0 0       0 ( UNIVERSAL::isa($ref, 'ARRAY') ? @$ref : $ref );
35             }
36              
37             sub parse_state_declarations {
38 1     1 0 2 my $self = shift;
39 1         7 $self->_parse_state_declarations($self->{top}, @_);
40 1         2 $self->_merge_any;
41 1         2 $self->_resolve_advances($self->{top});
42 1         4 $self->_resolve_transitions($self->{top}, []);
43             # $self->_propagate_transitions($self->{top});
44             }
45              
46             sub _parse_state_declarations {
47 1     1   2 my $self = shift;
48 1         2 my $parent = shift;
49 1         6 while (@_) {
50 0   0     0 my $name = shift // $self->_bad_def($parent, "undef is not valid as a state name");
51 0         0 my $decl = shift;
52 0 0       0 _is_hash($decl) or $self->_bad_def($parent, "HASH expected for substate '$name' declaration");
53 0         0 $self->_add_state($name, $parent, %$decl);
54             }
55             }
56              
57             sub _add_state {
58 0     0   0 my ($self, $name, $parent, @decl) = @_;
59 0         0 my $secondary;
60 0 0       0 if ($name =~ /^\((.*)\)$/) {
61 0         0 $name = $1;
62 0         0 $secondary = 1;
63             }
64 0         0 my $state = Class::StateMachine::Declarative::Builder::State->_new($name, $parent);
65 0 0       0 $self->_handle_attr_secondary($state, 1) if $secondary;
66 0         0 while (@decl) {
67 0         0 my $k = shift @decl;
68 0 0       0 my $method = $self->can("_handle_attr_$k") or $self->_bad_def($state, "bad declaration '$k'");
69 0 0       0 if (defined (my $v = shift @decl)) {
70 0 0 0     0 $debug and $debug & 16 and _debug($self, "calling handler for attribute $k with value $v");
71 0         0 $method->($self, $state, $v);
72             }
73             }
74 0         0 $self->{states}{$state->{full_name}} = $state;
75 0         0 $state;
76             }
77              
78             sub _ensure_event_is_free {
79 0     0   0 my ($self, $state, $event, $current) = @_;
80 0         0 my $seen;
81 0         0 for (qw(transitions on)) {
82 0 0       0 $seen = $_ if defined $state->{$_}{$event};
83             }
84 0         0 for (qw(delay ignore)) {
85 0 0       0 $seen = $_ if grep $_ eq $event, @{$state->{$_}};
  0         0  
86             }
87 0 0       0 if ($seen) {
88 0 0       0 unless (defined $current) {
89 0         0 $current = (caller 1)[3];
90 0         0 $current =~ s/^_handle_attr_//;
91             }
92 0 0       0 $seen and $self->_bad_def($state, "event '$event' appears on '$seen' and '$current' declarations");
93             }
94             }
95              
96             sub _handle_attr_enter {
97 0     0   0 my ($self, $state, $v) = @_;
98 0         0 $state->{enter} = $v;
99             }
100              
101             sub _handle_attr_leave {
102 0     0   0 my ($self, $state, $v) = @_;
103 0         0 $state->{leave} = $v;
104             }
105              
106             sub _handle_attr_jump {
107 0     0   0 my ($self, $state, $v) = @_;
108 0         0 $state->{jump} = $v;
109             }
110              
111             sub _handle_attr_advance {
112 0     0   0 my ($self, $state, $v) = @_;
113 0         0 $state->{advance} = $v;
114             }
115              
116             sub _handle_attr_delay {
117 0     0   0 my ($self, $state, $v) = @_;
118 0         0 my @events = _ensure_list($v);
119 0         0 $self->_ensure_event_is_free($state, $_) for @events;
120 0         0 push @{$state->{delay}}, @events;
  0         0  
121             }
122              
123             sub _handle_attr_ignore {
124 0     0   0 my ($self, $state, $v) = @_;
125 0         0 my @events = _ensure_list($v);
126 0         0 $self->_ensure_event_is_free($state, $_) for @events;
127 0         0 push @{$state->{ignore}}, @events;
  0         0  
128             }
129              
130             sub _handle_attr_secondary {
131 0     0   0 my ($self, $state, $v) = @_;
132 0         0 $state->{secondary} = !!$v;
133             }
134              
135             sub _handle_attr_before {
136 0     0   0 my ($self, $state, $v) = @_;
137 0 0       0 _is_hash($v) or $self->_bad_def($state, "HASH expected for 'before' declaration");
138 0         0 while (my ($event, $action) = each %$v) {
139 0 0       0 $state->{before}{$event} = $action if defined $action;
140             }
141             }
142              
143             sub _handle_attr_on {
144 0     0   0 my ($self, $state, $v) = @_;
145 0 0       0 _is_hash($v) or $self->_bad_def($state, "HASH expected for 'on' declaration");
146 0         0 while (my ($event, $action) = each %$v) {
147 0 0       0 if (defined $action) {
148 0         0 $self->_ensure_event_is_free($state, $event);
149 0         0 $state->{on}{$event} = $action;
150             }
151             }
152             }
153              
154             sub _handle_attr_transitions {
155 0     0   0 my ($self, $state, $v) = @_;
156 0 0       0 _is_hash($v) or $self->_bad_def($state, "HASH expected for 'transitions' declaration");
157 0         0 while (my ($event, $target) = each %$v) {
158 0 0       0 if (defined $target) {
159 0         0 $self->_ensure_event_is_free($state, $event);
160 0         0 $state->{transitions}{$event} = $target;
161             }
162             }
163             }
164              
165             sub _handle_attr_substates {
166 0     0   0 my ($self, $state, $v) = @_;
167 0 0       0 $state->{full_name} eq '/__any__' and $self->_bad_def($state, "pseudo state __any__ can not contain substates");
168 0 0       0 _is_array($v) or $self->_bad_def($state, "ARRAY expected for substate declarations");
169 0         0 $self->_parse_state_declarations($state, @$v);
170             }
171              
172             sub _merge_any {
173 1     1   2 my $self = shift;
174 1         2 my $top = $self->{top};
175 1         2 $top->{name} = '__any__';
176 1 50       5 if (defined(my $any = delete $self->{states}{'/__any__'})) {
177 0         0 my $ss = $self->{top}{substates};
178 0         0 @$ss = grep { $_->{name} ne '__any__' } @$ss;
  0         0  
179 0         0 delete $top->{$_} for qw(before transitions on);
180 0   0     0 $top->{$_} //= $any->{$_} for keys %$any;
181 0         0 $top->{$_} = $any->{$_} for qw(ignore delay);
182             }
183             }
184              
185             sub _resolve_advances {
186 1     1   10 my ($self, $state, $event) = @_;
187 1         2 my @ss = @{$state->{substates}};
  1         3  
188 1 50       4 if (@ss) {
189 0   0     0 $event = $state->{advance} // $event;
190 0         0 $self->_resolve_advances($_, $event) for @ss;
191 0 0       0 if (defined $event) {
192 0         0 while (@ss) {
193 0         0 my $current_state = shift @ss;
194 0 0       0 if (my ($next_state) = grep { not $_->{secondary} } @ss) {
  0         0  
195 0   0     0 $current_state->{transitions}{$event} //= $next_state->{full_name};
196             }
197             }
198             }
199             }
200             }
201              
202             sub _resolve_transitions {
203 1     1   2 my ($self, $state, $path) = @_;
204 1         2 my @path = (@$path, $state->{short_name});
205 1         1 my %transitions_abs;
206             my %transitions_rev;
207 1         2 while (my ($event, $target) = each %{$state->{transitions}}) {
  1         7  
208 0         0 my $target_abs = $self->_resolve_target($target, \@path);
209 0         0 $transitions_abs{$event} = $target_abs;
210 0   0     0 push @{$transitions_rev{$target_abs} ||= []}, $event;
  0         0  
211             }
212 1         2 $state->{transitions_abs} = \%transitions_abs;
213 1         2 $state->{transitions_rev} = \%transitions_rev;
214              
215 1         2 my $jump = $state->{jump};
216 1         1 my $ss = $state->{substates};
217 1 50 33     14 if (not defined $jump and not defined $state->{enter} and @$ss) {
      33        
218 0 0       0 if (my ($main) = grep { not $_->{secondary} } @$ss) {
  0         0  
219 0   0     0 $jump //= $main->{full_name};
220             }
221             else {
222 0         0 $self->_bad_def($state, "all the substates are secondary");
223             }
224             }
225              
226 1 50       4 $state->{jump_abs} = $self->_resolve_target($jump, \@path) if defined $jump;
227              
228 1         7 $self->_resolve_transitions($_, \@path) for @$ss;
229             }
230              
231             # sub _propagate_transitions {
232             # my ($self, $state) = @_;
233             # my $t = $state->{transitions_abs};
234             # for my $ss (@{$state->{substates}}) {
235             # my $ss_t = $ss->{transitions_abs};
236             # $ss_t->{$_} //= $t->{$_} for keys %$t;
237             # $self->_propagate_transitions($ss);
238             # }
239             # }
240              
241             sub _resolve_target {
242 0     0   0 my ($self, $target, $path) = @_;
243             # $debug and $debug & 32 and _debug($self, "resolving target '$target' from '".join('/',@$path)."'");
244 0 0       0 if ($target =~ m|^__(\w+)__$|) {
245 0         0 return $target;
246             }
247 0 0       0 if ($target =~ m|^/|) {
248 0 0       0 return $target if $self->{states}{$target};
249 0 0 0     0 $debug and $debug & 32 and _debug($self, "absolute target '$target' not found");
250             }
251             else {
252 0         0 my @path = @$path;
253 0         0 while (@path) {
254 0         0 my $target_abs = join('/', @path, $target);
255 0 0       0 if ($self->{states}{$target_abs}) {
256 0 0 0     0 $debug and $debug & 32 and _debug($self, "target '$target' from '".join('/',@$path)."' resolved as '$target_abs'");
257 0         0 return $target_abs;
258             }
259 0         0 pop @path;
260             }
261             }
262              
263 0         0 my $name = join('/', @$path);
264 0         0 $name =~ s|^/+||;
265 0         0 croak "unable to resolve transition target '$target' from state '$name'";
266             }
267              
268             sub generate_class {
269 1     1 0 2 my $self = shift;
270 1         3 $self->_generate_state($self->{top});
271             }
272              
273             sub _generate_state {
274 1     1   2 my ($self, $state) = @_;
275 1         2 my $class = $self->{class};
276 1         2 my $name = $state->{name};
277 1         1 my $parent = $state->{parent};
278 1 50       4 my $parent_name = ($parent ? $parent->{name} : undef);
279 1 50 0     7 $debug and $debug & 16 and _debug("generating subs for class $class, state $name, parent: ". ($parent_name // ''));
      33        
280              
281 1 50 33     3 if ($parent and $parent_name ne '__any__') {
282 0         0 Class::StateMachine::set_state_isa($class, $name, $parent_name);
283             }
284              
285 1         3 for my $when ('enter', 'leave') {
286 2 50       11 if (defined (my $action = $state->{$when})) {
287             Class::StateMachine::install_method($class,
288             "${when}_state",
289 0     0   0 sub { shift->$action },
290 0         0 $name);
291             }
292             }
293              
294 1 50 33     8 if (!defined $state->{enter} and $name ne '__any__') {
295 0 0       0 if (defined (my $jump = $state->{jump_abs})) {
296 0         0 my $name = $state->{name};
297 0         0 my $jump_name = $self->{states}{$jump}{name};
298 0 0 0     0 $debug and $debug & 32 and _debug(__PACKAGE__, "installing handler for jump(=> $jump_name) at $class/$name");
299             Class::StateMachine::install_method($class,
300             'enter_state',
301             sub {
302 0     0   0 my $self = shift;
303 0 0       0 if ($self->state eq $name) {
304 0 0 0     0 $debug and $debug & 64 and _debug($self, "jumping to state $jump_name");
305 0         0 $self->state($jump_name)
306             }
307             else {
308 0 0 0     0 $debug and $debug & 64 and
309             _debug(64, "skipping jump to state $jump_name set for state $name");
310             }
311             },
312 0         0 $name);
313             }
314             }
315              
316 1         13 for my $event (keys %{$state->{before}}) {
  1         5  
317 0         0 my $action = $state->{before}{$event};
318 0         0 my $event1 = $event;
319             my $sub = sub {
320 0     0   0 my $self = shift;
321 0 0       0 if (my $method = $self->next::can) {
322 0 0       0 $self->state_changed_on_call($method, $self) and return;
323             }
324 0         0 $self->$action;
325 0         0 };
326 0 0 0     0 $debug and $debug & 32 and _debug(__PACKAGE__, "installing handler for before($event1 => $action) at $class/$name");
327 0         0 Class::StateMachine::install_method($class,
328             "$event/before",
329             $sub,
330             $name);
331             }
332              
333 1         2 for my $event (@{$state->{delay}}) {
  1         3  
334 0         0 my $event1 = $event;
335 0 0 0     0 $debug and $debug & 32 and _debug(__PACKAGE__, "installing handler for delay($event1) at $class/$name");
336             Class::StateMachine::install_method($class,
337             $event,
338             sub {
339 0     0   0 my $self = shift;
340 0 0 0     0 $debug and $debug & 64 and _debug($self, "event $event1 received (delay)");
341 0         0 $self->delay_until_next_state($event1) },
342 0         0 $name);
343             }
344              
345 1         2 for my $event (keys %{$state->{on}}) {
  1         3  
346 0         0 my $action = $state->{on}{$event};
347 0         0 my $before = "$event/before";
348 0         0 my $event1 = $event;
349             my $sub = sub {
350 0     0   0 my $self = shift;
351 0 0 0     0 $debug and $debug & 64 and _debug($self, "event $event1 received (on target: $action)");
352 0 0       0 if (my $method = $self->can($before)) {
353 0 0       0 $self->state_changed_on_call($method, $self, @_) and return;
354             }
355 0         0 $self->$action(@_);
356 0         0 };
357 0 0 0     0 $debug and $debug & 32 and _debug(__PACKAGE__, "installing handler for on($event1 => $action) at $class/$name");
358 0         0 Class::StateMachine::install_method($class, $event, $sub, $name);
359             }
360              
361 1         2 for my $event (@{$state->{ignore}}) {
  1         4  
362 0         0 my $before = "$event/before";
363 0         0 my $event1 = $event;
364             my $sub = sub {
365 0     0   0 my $self = shift;
366 0 0 0     0 $debug and $debug & 64 and _debug($self, "event $event1 received (ignore)");
367 0         0 my $method = $self->can($before);
368 0 0       0 $self->$method(@_) if $method;
369 0         0 };
370 0         0 Class::StateMachine::install_method($class, $event, $sub, $name);
371             }
372              
373 1         2 while (my ($target, $events) = each %{$state->{transitions_rev}}) {
  1         4  
374 0         0 my $target = $self->{states}{$target}{name};
375 0         0 for my $event (@$events) {
376 0         0 my $before = "$event/before";
377 0         0 my $event1 = $event;
378             my $sub = sub {
379 0     0   0 my $self = shift;
380 0 0 0     0 $debug and $debug & 64 and _debug($self, "event $event1 received (transition target: $target)");
381 0 0       0 if (my $method = $self->can($before)) {
382 0 0       0 $self->state_changed_on_call($method, $self, @_) and return;
383             }
384 0         0 $self->state($target);
385 0         0 };
386 0 0 0     0 $debug and $debug & 32 and _debug(__PACKAGE__, "installing handler for transition($event1 => $target) at $class/$name");
387 0         0 Class::StateMachine::install_method($class, $event, $sub, $name);
388             }
389             }
390              
391 1         2 $self->_generate_state($_) for @{$state->{substates}};
  1         26  
392             }
393              
394             package Class::StateMachine::Declarative::Builder::State;
395              
396             sub _new {
397 1     1   2 my ($class, $name, $parent) = @_;
398 1   50     7 $name //= '';
399 1 50       5 my $full_name = ($parent ? "$parent->{full_name}/$name" : $name);
400 1         1 my $final_name = $full_name;
401 1         2 $final_name =~ s|^/+||;
402 1         8 my $state = { short_name => $name,
403             full_name => $full_name,
404             name => $final_name,
405             parent => $parent,
406             substates => [],
407             transitions => {},
408             before => {},
409             on => {},
410             ignore => [],
411             delay => [] };
412 1         3 bless $state, $class;
413 1 50       4 push @{$parent->{substates}}, $state if $parent;
  0         0  
414 1         9 Scalar::Util::weaken($state->{parent});
415 1         3 $state;
416             }
417              
418             1;