File Coverage

blib/lib/Class/StateMachine.pm
Criterion Covered Total %
statement 260 297 87.5
branch 51 120 42.5
condition 10 28 35.7
subroutine 48 53 90.5
pod 2 2 100.0
total 371 500 74.2


line stmt bran cond sub pod time code
1             package Class::StateMachine;
2              
3             package Class::StateMachine::Private;
4              
5             sub _eval_states {
6             # we want the state declarations evaluated inside a clean
7             # environment (lexicaly free):
8 1     1   6 eval $_[0]
  1     1   2  
  1     1   29  
  1     1   5  
  1     1   2  
  1     1   57  
  1     1   4  
  1     1   2  
  1     1   21  
  1     1   4  
  1     1   1  
  1     1   20  
  1     1   28  
  1     13   2  
  1         20  
  1         5  
  1         1  
  1         35  
  1         4  
  1         1  
  1         21  
  1         4  
  1         2  
  1         19  
  1         5  
  1         1  
  1         24  
  1         6  
  1         2  
  1         24  
  1         5  
  1         7  
  1         22  
  1         4  
  1         2  
  1         19  
  1         5  
  1         2  
  1         25  
  13         757  
9             }
10              
11             package Class::StateMachine;
12              
13             our $VERSION = '0.24';
14              
15             our $debug //= 0;
16             our $ignore_same_state_changes //= 0;
17              
18             package Class::StateMachine::Private;
19              
20 1     1   19750 use 5.010001;
  1         3  
  1         37  
21              
22 1     1   6 use strict;
  1         1  
  1         37  
23 1     1   4 use warnings;
  1         15  
  1         25  
24 1     1   4 use Carp;
  1         1  
  1         117  
25 1     1   14 BEGIN { our @CARP_NOT = qw(Class::StateMachine) }
26 1     1   773 use mro;
  1         694  
  1         5  
27 1     1   672 use MRO::Define;
  1         448  
  1         26  
28 1     1   821 use Hash::Util qw(fieldhash);
  1         2191  
  1         5  
29 1     1   3138 use Devel::Peek 'CvGV';
  1         422  
  1         5  
30 1     1   725 use Package::Stash;
  1         8862  
  1         30  
31 1     1   7 use Sub::Name;
  1         1  
  1         48  
32 1     1   5 use Scalar::Util qw(refaddr);
  1         1  
  1         1264  
33              
34             fieldhash my %state;
35             fieldhash my %state_changed;
36             fieldhash my %delayed;
37             fieldhash my %delayed_once;
38             fieldhash my %on_leave_state;
39              
40             my %class_bootstrapped;
41             my %class_state_isa;
42              
43             sub _debug {
44 0     0   0 my $self = shift;
45 0         0 require Time::HiRes;
46 0 0       0 if (length(my $class = Class::StateMachine::ref($self))) {
47 0   0     0 my $state = $state{$self} // '';
48 0         0 my $addr = refaddr($self);
49 0 0       0 warn sprintf "%08.3f %s[%x/%s%s]> %s\n",
50             Time::HiRes::time(), $class, $addr, $state, ($state_changed{$self} ? '|sc' : ''), "@_";
51             }
52             else {
53 0         0 warn sprintf "%08.3f %s> %s\n", Time::HiRes::time(), $self, "@_";
54             }
55             }
56              
57             sub _state {
58 10     10   16 my($self, $new_state) = @_;
59 10   50     32 $state{$self} // croak("object $self has no state, " .
60             "use Class::StateMachine::bless to create Class::StateMachine objects");
61 10 100       22 if (defined $new_state) {
62 9         13 my $old_state = $state{$self};
63 9 50 33     20 return $old_state if $ignore_same_state_changes and $new_state eq $old_state;
64 9 50       19 $debug and _debug($self, "changing state from $old_state to $new_state");
65 9         37 $state_changed{$self} = 1;
66 9         55 local $state_changed{$self};
67              
68 9 50       44 if (my $check = $self->can('check_state')) {
69 0 0       0 $debug and _debug($self, "checking state $new_state");
70 0 0       0 $check->($self, $new_state) or croak qq(invalid state "$new_state");
71 0 0       0 return $state{$self} if $state_changed{$self};
72             }
73 9 50       42 if (my $leave = $self->can('leave_state')) {
74 9 50       13 $debug and _debug($self, "calling leave_state($old_state, $new_state)");
75 9         26 $leave->($self, $old_state, $new_state);
76 9 50       657 return $state{$self} if $state_changed{$self};
77             }
78 9 50       27 if (my $on_leave = $on_leave_state{$self}) {
79 0         0 while (defined(my $cb_and_args = shift @$on_leave)) {
80 0         0 my $cb = shift @$cb_and_args;
81 0 0       0 $debug and _debug($self, "calling on_leave_state hook $cb");
82 0 0       0 ref $cb ? $cb->(@$cb_and_args) : $self->$cb(@$cb_and_args);
83 0 0       0 return $state{$self} if $state_changed{$self};
84             }
85             }
86              
87 9         16 my $base_class = ref($self);
88 9         49 $base_class =~ s|::__state__::.*$||;
89 9         18 my $class = _bootstrap_state_class($base_class, $new_state);
90 9         27 $state{$self} = $new_state;
91 9         17 bless $self, $class;
92 9 50       29 $debug and _debug($self, "real class set to $class");
93              
94 9         14 my $delayed = $delayed{$self};
95 9 100       19 my $delayed_top = ($delayed ? $#$delayed : -1);
96              
97 9 50       38 if (my $enter = $self->can('enter_state')) {
98 9 50       17 $debug and _debug($self, "calling enter_state($new_state, $old_state)");
99 9         25 $enter->($self, $new_state, $old_state);
100 9 50       609 $debug and _debug($self, "back from enter_state($new_state, $old_state)");
101 9 50       32 return $state{$self} if $state_changed{$self};
102             }
103              
104 9         44 for (0..$delayed_top) {
105 2         14 my $action = shift @$delayed;
106 2 50       8 $debug and _debug($self, "running delayed action $action");
107 2         11 $self->$action;
108 2 50       19 return $state{$self} if $state_changed{$self};
109             }
110             }
111              
112 10 50       21 $debug and _debug($self, "state set to $state{$self}");
113 10         26 return $state{$self};
114             }
115              
116             sub _bless {
117 1     1   16 my ($self, $base_class, $state) = @_;
118 1   33     6 $base_class //= caller;
119 1 50       3 if (defined $state) {
120 0 0       0 defined $state{$self} and croak "unable to change state when reblessing";
121 0         0 $state{$self} = $state;
122             }
123             else {
124 1   50     14 $state{$self} //= 'new';
125             }
126 1         5 my $class = _bootstrap_state_class($base_class, $state{$self});
127 1         3 bless $self, $class;
128 1 50       3 $debug and _debug($self, "real class set to $class");
129 1         3 $self;
130             }
131              
132             sub _delay {
133 2     2   4 my $self = shift;
134 2         3 my $code;
135 2 100       6 if (@_) {
136 1         1 $code = shift;
137 1 50       3 @_ and croak 'Usage: $self->delay_until_next_state($method_or_cb)';
138 1 50       5 defined $code or return;
139             }
140             else {
141 1         5 $code = (caller 1)[3];
142 1         7 $code =~ s/.*:://;
143             }
144 2   100     14 my $delayed = ($delayed{$self} //= []);
145 2         8 push @$delayed, $code;
146             }
147              
148             sub _delay_once {
149 2     2   8 my $self = shift;
150 2         2 my $method;
151 2 50       6 if (@_) {
152 0         0 $method = shift;
153 0 0       0 @_ and croak 'Usage: $self->delay_once_until_next_state($method)';
154 0 0       0 defined $method or return;
155 0 0       0 croak "_delay_once does not accept code refs" if ref $method;
156             }
157             else {
158 2         9 $method = (caller 1)[3];
159 2         12 $method =~ s/.*:://;
160             }
161 2 100       17 $delayed_once{$self}{$method}++
162             or _delay($self, $method);
163             }
164              
165             sub _on_leave_state {
166 0     0   0 my $self = shift;
167 0 0       0 @_ or croak 'Usage: $self->on_leave_state($callback, @args)';
168 0 0 0     0 push @{$on_leave_state{$self} //= []}, [@_] if defined $_[0];
  0         0  
169             }
170              
171             sub _state_changed_on_call {
172 0     0   0 my $self = shift;
173 0         0 my $cb = shift;
174 0 0       0 local $state_changed{$self} if $state_changed{$self};
175 0 0       0 ref $cb ? $cb->(@_) : $self->$cb(@_);
176 0         0 $state_changed{$self};
177             }
178              
179             sub _bootstrap_state_class {
180 10     10   16 my ($class, $state) = @_;
181 10         19 my $state_class = "${class}::__state__::${state}";
182 10 100       26 unless ($class_bootstrapped{$state_class}) {
183             # disallow control characters and colons inside state names:
184 8 50       36 $state =~ m|^[\x21-\x39\x3b-\x7f]+$| or croak "'$state' is not a valid state name";
185 8         18 $class_bootstrapped{$state_class} = 1;
186              
187 1     1   5 no strict 'refs';
  1         2  
  1         382  
188 8         9 @{$state_class.'::ISA'} = $class;
  8         176  
189 8         12 ${$state_class.'::state'} = $state;
  8         37  
190 8         8 ${$state_class.'::base_class'} = $class;
  8         34  
191 8         9 ${$state_class.'::state_class'} = $state_class;
  8         25  
192 8         29 mro::set_mro($state_class, 'statemachine');
193             }
194 10         22 return $state_class;
195             }
196              
197             my @state_methods;
198              
199             sub _handle_attr_OnState {
200 13     13   25 my ($class, $sub, $on_state) = @_;
201 13         46 my ($filename, $line) = (caller 2)[1,2];
202 13         17 my ($err, @on_state);
203 13         13 do {
204 13         12 local $@;
205 13         39 @on_state = _eval_states <
206             package $class;
207             no warnings 'reserved';
208             # line $line $filename
209             $on_state;
210             EOE
211 13         84 $err = $@;
212             };
213 13 50       26 croak $err if $err;
214 13 50       32 grep(!defined, @on_state) and croak "undef is not a valid state";
215 13 50       23 @on_state or warnings::warnif('Class::StateMachine',
216             'no states on OnState attribute declaration');
217 13         83 push @state_methods, [$class, undef, $sub, @on_state];
218             }
219              
220             sub _move_state_methods {
221 1   33 1   136 $_->[1] //= CvGV $_->[2] for @state_methods;
222 1         5 while (@state_methods) {
223 13         16 my ($class, $sym, $sub, @on_state) = @{shift @state_methods};
  13         53  
224 13 50       92 my ($method) = $sym=~/([^:]+)$/ or croak "invalid symbol name '$sym'";
225              
226 13         85 my $stash = Package::Stash->new($class);
227 13         99 $stash->remove_symbol("&$method");
228              
229 13         25 for my $state (@on_state) {
230 15         27 my $methods_class = join('::', $class, '__methods__', $state);
231 15         22 my $full_name = "${methods_class}::$method";
232             # print "registering method at $full_name\n";
233 1     1   5 no strict 'refs';
  1         2  
  1         190  
234 15         194 *$full_name = subname($full_name, $sub);
235             }
236             }
237             }
238              
239             sub _set_state_isa {
240 1     1   17 my ($class, $state, @isa) = @_;
241 1         4 %class_bootstrapped = ();
242 1 50       10 mro::method_changed_in($class) if mro::get_pkg_gen($class);
243 1         5 $class_state_isa{$class}{$state} = \@isa;
244             }
245              
246             sub _state_isa {
247 1     1   2 my ($class, $state) = @_;
248 1 50       4 if (my $ref = ref $class) {
249 1   33     7 $state //= $class->state;
250 1     1   5 no strict 'refs';
  1         2  
  1         321  
251 1         2 $class = ${$ref . "::base_class"};
  1         5  
252             }
253 1         2 _state_isa_from_derived([grep { $_->isa('Class::StateMachine') } @{mro::get_linear_isa($class)}],
  3         12  
  1         3  
254             $state);
255             }
256              
257              
258             sub _state_isa_from_derived {
259 61     61   77 my ($derived, $state) = @_;
260 61         52 my (@isa, @queue, %seen);
261 61         57 do {
262 69 50       188 unless ($seen{$state}++) {
263 69         85 push @isa, $state;
264 69         99 for my $class (@$derived) {
265 199 100       558 if (my $isa = $class_state_isa{$class}{$state}) {
266 8         11 push @queue, @$isa;
267 8         23 last;
268             }
269             }
270             }
271             } while (defined($state = shift @queue));
272 61         72 push @isa, '__any__';
273 61 50       205 wantarray ? @isa : $isa[1];
274             }
275              
276             # we make $^V into a constant so that it gets optimized away at
277             # compile time:
278 1     1   5 use constant _perl_version => $^V;
  1         2  
  1         288  
279              
280             # use Data::Dumper;
281             sub _statemachine_mro {
282 60     60   6217 my $stash = shift;
283 60 100       119 _move_state_methods if @state_methods;
284 60         86 my $base_class = ${$stash->{base_class}};
  60         160  
285 60         80 my $state = ${$stash->{state}};
  60         98  
286 60         69 my @linear = @{mro::get_linear_isa($base_class)};
  60         217  
287 60         84 my @derived = grep { $_->isa('Class::StateMachine') } @linear;
  180         638  
288 60         63 my @classes;
289 60         103 for my $state (_state_isa_from_derived(\@derived, $state)) {
290 127         566 push @classes, map join('::', $_, '__methods__', $state), @derived;
291             }
292              
293 60         1304 ( _perl_version >= 5.016000
294             ? [ grep mro::get_pkg_gen($_), @classes, @linear ]
295             # workaround bug on early mro implementations where the first
296             # class on the list returned was always discarded. Also, as we may
297             # have inserted methods from this callback, the state class should
298             # be searched again, so we hardcode it in the list even when empty.
299             : [ $classes[0], grep mro::get_pkg_gen($_), @classes, @linear ] )
300             }
301              
302             MRO::Define::register_mro('statemachine' => \&_statemachine_mro);
303              
304             package Class::StateMachine;
305 1     1   5 use warnings::register;
  1         2  
  1         348  
306              
307             sub MODIFY_CODE_ATTRIBUTES {
308 13     13   7450 my ($class, undef, @attr) = @_;
309 13 50       18 grep { !/^OnState\((.*)\)$/
  13         69  
310             or (Class::StateMachine::Private::_handle_attr_OnState($class, $_[1], $1), 0) } @attr;
311             }
312              
313             *state = \&Class::StateMachine::Private::_state;
314             *rebless = \&Class::StateMachine::Private::_bless;
315             *bless = \&Class::StateMachine::Private::_bless;
316             *delay_until_next_state = \&Class::StateMachine::Private::_delay;
317             *delay_once_until_next_state = \&Class::StateMachine::Private::_delay_once;
318             *on_leave_state = \&Class::StateMachine::Private::_on_leave_state;
319             *state_changed_on_call = \&Class::StateMachine::Private::_state_changed_on_call;
320             *set_state_isa = \&Class::StateMachine::Private::_set_state_isa;
321             *state_isa = \&Class::StateMachine::Private::_state_isa;
322              
323             sub ref {
324 1     1 1 3 my $class = ref $_[0];
325 1 50       4 return '' if $class eq '';
326 1     1   6 no strict 'refs';
  1         1  
  1         209  
327 1   33     2 ${$class .'::base_class'} // $class;
  1         7  
328             }
329              
330 0     0   0 sub DESTROY {}
331 1     1   16 sub import {}
332              
333             sub AUTOLOAD {
334 1     1   2 our $AUTOLOAD;
335 1         38 my $self = $_[0];
336 1 50 33     13 if (CORE::ref $self and defined $state{$self}) {
337 1         17 my $state = $state{$self};
338 1         2 my $method = $AUTOLOAD;
339 1         7 $method =~ s/.*:://;
340 1         3 my $state_class = CORE::ref($self);
341 1         3 my @state_mro = @{mro::get_linear_isa($state_class)};
  1         7  
342 1         4 my $base_class = Class::StateMachine::ref($self);
343 1         2 my @base_mro = @{mro::get_linear_isa($base_class)};
  1         5  
344              
345 1         2 my @submethods;
346 1         5 for my $class (@base_mro) {
347 1     1   9 no strict 'refs';
  1         2  
  1         408  
348 3 100       3 if (exists ${"${class}::"}{"__methods__::"}) {
  3         14  
349 2         51 my $methods = "${class}::__methods__::";
350 2         18 for my $state (grep /::$/, keys %$methods) {
351 10 100       12 exists ${"$methods$state"}{$method} and
  10         40  
352             push @submethods, "$methods$state$method";
353             }
354             }
355 3 50       5 defined *{"${class}::$method"}{CODE} and
  3         18  
356             push @submethods, "${class}::$method";
357             }
358              
359 1         13 my $error = join("\n",
360             qq|Can't locate Class::StateMachine object method "$method" via package "$base_class" ("$state_class") for object in state "$state"|,
361             "The base mro is:",
362             " " . join("\n ", @base_mro),
363             "The state mro is:",
364             " " . join("\n ", @state_mro),
365             "The submethods on the inheritance chain are:",
366             " " . join("\n ", @submethods),
367             "...");
368 1         2 local $Carp::Verbose = 1;
369 1         190 Carp::croak $error;
370             }
371             else {
372 0           Carp::croak "Undefined subroutine &$AUTOLOAD called"
373             }
374             }
375              
376             sub install_method {
377 0     0 1   my ($class, $name, $sub, @states) = @_;
378 0 0         CORE::ref($class) and Carp::croak "$class is not a package valid package name";
379 0 0         CORE::ref($sub) eq 'CODE' or Carp::croak "$sub is not a subroutine reference";
380 0           push @state_methods, [$class, $name, $sub, @states];
381 0           Class::StateMachine::Private::_move_state_methods;
382             }
383              
384             1;
385             __END__