File Coverage

blib/lib/Basset/Machine.pm
Criterion Covered Total %
statement 9 65 13.8
branch 0 38 0.0
condition 0 6 0.0
subroutine 3 16 18.7
pod 13 13 100.0
total 25 138 18.1


line stmt bran cond sub pod time code
1             package Basset::Machine;
2              
3             #Basset::Machine, copyright and (c) 2004, 2005, 2006 James A Thomason III
4             #Basset::Machine is distributed under the terms of the Perl Artistic License.
5              
6             =pod
7              
8             =head1 NAME
9              
10             Basset::Machine - used to state machines
11              
12             =head1 AUTHOR
13              
14             Jim Thomason, jim@jimandkoka.com
15              
16             =head1 DESCRIPTION
17              
18             Basset::Machine implements a state machine. This is useful for any thing that requires a process flow.
19             web applications, shell scripts, tk apps, you name it. Anything that you want that requires user
20             interaction and a flow of control.
21              
22             =head1 SYNOPSIS
23              
24             An example is best. Let's try a simple one.
25              
26             package My::Machine;
27             use Basset::Object;
28             Basset::Object->inherits(__PACKAGE__, 'machine');
29            
30             sub start {
31             return shift->state('login');
32             }
33            
34             sub login {
35             my $self = shift;
36             my $heap = $self->heap;
37            
38             if ($heap->{'loggedin'}) {
39             return $self->state('success');
40             } else {
41             return $self->state('prompt');
42             }
43             }
44            
45             sub prompt {
46             my $self = shift;
47             print "Please enter your username (must be 'bob'): ";
48             chomp(my $name = );
49            
50             $self->{'heap'}->{'loggedin'} = 1 if $name eq 'bob';
51             return $self->state('login');
52             }
53            
54             sub success {
55             my $self = shift;
56            
57             print "You are logged in\n";
58            
59             return $self->terminate;
60             }
61            
62             1;
63            
64             ---
65            
66             #!/usr/bin/perl
67             use My::Machine;
68            
69             My::Machine->execute;
70              
71             Look at the L for more info.
72              
73             =cut
74              
75 1     1   1720 use strict;
  1         3  
  1         35  
76 1     1   6 use warnings;
  1         2  
  1         44  
77              
78             our $VERSION = '1.01';
79              
80 1     1   6 use Basset::Object;
  1         2  
  1         1313  
81             our @ISA = Basset::Object->pkg_for_type('object');
82              
83             =pod
84              
85             =head1 ATTRIBUTES
86              
87             =over
88              
89             =item state
90              
91             This is the current state of the machine. This is how you move around in the flow of your
92             machine. The default start state is 'start'. If you don't provide a state when your machine
93             starts executing, it will try to enter the start state. You may always provide the state that you
94             want. It is traditional to return the next state from your current state.
95              
96             sub current_state {
97             my $self = shift;
98            
99             return $self->state('next_state');
100             }
101              
102             States are usually methods in your machine module. But, if you have a complicated state, you may
103             put it into its own class, in a subdirectory of the Machine class. methods in the machine class take
104             precedence over external states. External states are entered via their 'main' method.
105              
106             package My::Machine;
107            
108             sub some_state {
109             return shift->state('login');
110             }
111            
112             package My::Machine::Login;
113            
114             sub main {
115             my $self = shift;
116             my $machine = $self->m;
117            
118             return $self->m->state('jump_pt');
119             }
120              
121             All machines implicitly start in a setup state when they begin running, and end with a terminate state,
122             if those are defined. Note that these states will be entered any time the machine starts or stops running,
123             respectively, so you may need to explicitly check the current state as appropriate.
124              
125             When entering a state, you receive 1 argument - the state you came from. You may receive additional
126             arguments that the prior state handed in to you.
127              
128             See Basset::Machine::State for more information.
129              
130             =cut
131              
132             __PACKAGE__->add_attr('_state');
133              
134             sub state {
135 0     0 1   my $self = shift;
136            
137 0 0         if (@_) {
138 0           my $last = $self->_state;
139 0           $self->_state(shift);
140 0           return ($last, @_);
141             }
142            
143 0           return $self->_state;
144             }
145              
146             =pod
147              
148             =item heap
149              
150             The heap is a hashref that contains useful information that's local to the machine. You can
151             think of it as a global namespace as far as the states are concerned, but local to the machine.
152              
153             This is how data is passed from state to state.
154              
155             sub state1 {
156             my $self = shift;
157             $self->heap->{'value1'} = 'foo';
158            
159             return $self->state('state2');
160             }
161            
162             sub state2 {
163             my $self = shift;
164            
165             print 'value1 is ', $self->heap->{'value1'}, "\n";
166            
167             return $self->terminate;
168             }
169              
170             =cut
171              
172             __PACKAGE__->add_attr('heap');
173              
174             #Boolean flag. This determines if the machine is currently running. It's automatically set as
175             #the machine starts and stops. You should never need to worry about it.
176              
177             __PACKAGE__->add_attr('running');
178              
179             =pod
180              
181             =item transitions
182              
183             transitions provides a layer of insulation for you. Instead of explicitly specifying your
184             machine's states in code (for example, in a web app where every html page needs to return the
185             machine's state), you can instead define a transition. This allows you to hide the actual states
186             from the external world. So you can re-define states as desired, but the transitions will always
187             remain the same.
188              
189             My::Machine->transitions({
190             'login' => 'login_prompt',
191             'analyze' => 'analyze_2', #changed from old 'analyze' method
192             });
193              
194             You then invoke it via a transition call, instead of a state call.
195              
196             sub state {
197             my $self = shift;
198            
199             return $self->transition('analyze');
200             }
201              
202             =cut
203              
204             __PACKAGE__->add_trickle_class_attr('transitions', {});
205              
206             =pod
207              
208             =item reentry_is_fatal
209              
210             object attribute, which defaults to true. Normally, this will prevent you from re-entering
211             a state from itself. Most of the time, this means that you forgot to transition out of it
212             at the end of the state.
213              
214             Nonetheless, there are times when you may want to stay where you are. If you have a machine
215             that functions that way, then make this attribute false, and best of luck to you.
216              
217             =back
218              
219             =cut
220              
221             __PACKAGE__->add_attr('reentry_is_fatal');
222              
223             =pod
224              
225             =item extractor
226              
227             Most machines tend to need extractors. So you have one for free here. Wrappered by the extract method, below.
228              
229             =cut
230              
231             __PACKAGE__->add_attr('extractor');
232              
233             =pod
234              
235             =begin btest(extractor)
236              
237              
238             =end btest(extractor)
239              
240             =cut
241              
242              
243             sub init {
244             return shift->SUPER::init(
245 0     0 1   'running' => 0,
246             'heap' => {},
247             'state' => 'start',
248             'reentry_is_fatal' => 1,
249             @_
250             );
251             }
252              
253             =pod
254              
255             =head1 METHODS
256              
257             =over
258              
259             =item execute
260              
261             convenience method which allows you to create and run a machine in one step.
262              
263             My::Machine->execute();
264            
265             is the same as:
266              
267             my $m = My::Machine->new();
268             $m->run();
269              
270             Will return undef if the machine aborts or is not constructed, and the machine itself upon its
271             termination.
272              
273             =cut
274              
275             sub execute {
276 0     0 1   my $class = shift;
277 0 0         my $m = $class->new(@_) or return;
278            
279 0 0         $m->run() or return $class->error($m->errvals);
280            
281 0           return $m;
282             }
283              
284             =pod
285              
286             =item run
287              
288             Actually runs the machine, transitions states, does all the magic.
289              
290             $machine->run;
291              
292             =cut
293              
294             sub run {
295 0     0 1   my $self = shift;
296              
297 0           $self->running(1);
298              
299 0           my @rc = (undef); #that way, the start state will always reflect that it came from nothing.
300              
301 0 0         $self->setup or $self->abort;
302              
303 0           while ($self->running) {
304 0 0         my $state = defined $self->state ? $self->state : 'start';
305 0 0         if ($self->can($state)) {
306 0 0         @rc = $self->$state(@rc) or return;
307             } else {
308 0           my $state_pkg = $self->pkg . "::" . ucfirst($state);
309 0 0         if (ref $state eq 'ARRAY') {
310 0           ($state, $state_pkg) = @$state;
311             }
312            
313 0 0         $self->load_pkg($state_pkg) or return $self->abort("Cannot jump to $state : not defined (" . $self->error . ")", $self->errcode);
314            
315 0           my $state_obj = $state_pkg->new();
316 0           $state_obj->machine($self);
317 0 0         @rc = $state_obj->main(@rc) or return $self->abort($state_obj->errvals);
318             }
319            
320             #if reentry is fatal, and we haven't moved (same state, or still at start, we bomb)
321 0 0 0       if (((defined $self->state && $state eq $self->state) || ($state eq 'start' && ! defined $self->state)) && $self->reentry_is_fatal) {
      0        
322 0           return $self->abort("Attempted to re-enter $state. Did you forget to transition?", "BM-07");
323             }
324            
325             }
326            
327 0 0         $self->teardown or return;
328            
329 0 0         return wantarray ? @rc : $rc[0];
330            
331             }
332              
333             =pod
334              
335             =item setup
336              
337             implicit state that executes when the machine starts running. Does not actually affect the current
338             state of the machine (that is, you can check $self->state and it won't return 'setup'). By default,
339             it just returns success and the machine then begins running.
340              
341             This is a good place to do things like setup database connections, look up frequently used classes,
342             cache data, etc. By default, you get your extractor attribute set to whatever's in your conf file.
343              
344             If setup aborts, it will teardown the machine and nothing will run.
345              
346             =cut
347              
348             sub setup {
349 0     0 1   my $self = shift;
350            
351 0           $self->extractor($self->pkg_for_type('extractor'));
352            
353 0           return $self;
354             }
355              
356             =pod
357              
358             =item teardown
359              
360             implicit state that executes when the machine stops running. Will receive no arguments if the machine
361             terminates normally (terminate or interrupt), will receive the single word "aborted" if the machine is stopping due to an abort.
362             Does not actually affect the last run state (that is, you can check $self->state and it won't return
363             'teardown'). By default, it just returns success and the machine is done running.
364              
365             This is a good place to do things like close database connections, write things to disk, log messages,
366             etc.
367              
368             =cut
369              
370             sub teardown {
371 0     0 1   return 1;
372             }
373              
374             =pod
375              
376             =item start
377              
378             start is the only state that must be defined within the machine class itself. This super method is
379             abstract and aborts the machine. You must override it.
380              
381             =cut
382              
383             sub start {
384 0     0 1   my $self = shift;
385            
386 0           return $self->abort("Cannot enter start state : not defined", "BM-01");
387             }
388              
389             =pod
390              
391             =item terminate
392              
393             terminate stops the machine normally and clears out the current state.
394              
395             =cut
396              
397             sub terminate {
398 0     0 1   my $self = shift;
399              
400 0           $self->running(0);
401 0           $self->state(undef);
402            
403 0           return 'terminated';
404             }
405              
406             =pod
407              
408             =item interrupt
409              
410             interrupt expects to be given a state. It will stop the machine from running, and advance it to
411             the state that was provided. This is useful to temporarily suspend the machine and return to it
412             later. Note that re-running the machine will cause setup to be re-run, and that you will still
413             run teardown after the interrupt.
414              
415             =cut
416              
417             sub interrupt {
418 0     0 1   my $self = shift;
419 0 0         my $state = shift or return $self->abort("Cannot interrupt w/o next state", "BM-02");
420            
421 0           $self->state($state);
422            
423 0           $self->running(0);
424            
425 0           return $state;
426             }
427              
428             =pod
429              
430             =item abort
431              
432             aborts the machine immediately, tears it down, and returns the error passed in. This should be
433             used to report machine errors in place of ->error.
434              
435             =cut
436              
437             sub abort {
438 0     0 1   my $self = shift;
439 0 0         $self->teardown('aborted') or return;
440 0           return $self->error(@_);
441             }
442              
443             =pod
444              
445             =item machine
446              
447             simply returns self. This is a convenience method to make states more readily interchangeable between
448             methods and explicit state modules
449              
450             =cut
451              
452             sub machine {
453 0     0 1   return shift;
454             }
455              
456             =pod
457              
458             =item transition
459              
460             transitions the machine to the next state, as per the transitions table.
461              
462             $m->transition('login');
463              
464             =cut
465              
466             sub transition {
467 0     0 1   my $self = shift;
468 0 0         my $key = shift or return $self->abort("Cannot transition w/o key", "BM-03");
469            
470 0 0         my $next_state = $self->transitions->{$key}
471             or return $self->abort("Cannot transition: no state jump for key $key", "BM-05");
472              
473 0           return $self->state($next_state, @_);
474             }
475              
476             =pod
477              
478             =item extract
479              
480             Convenience method. Simply calls extract on your extractor attribute, if you have one.
481              
482             =cut
483              
484             sub extract {
485 0     0 1   my $self = shift;
486 0 0         my $extractor = $self->extractor or return $self->error("Cannot extract w/o extractor", "XXX");
487            
488 0 0         return $extractor->extract(@_) or $self->error($extractor->errvals);
489             }
490              
491             =pod
492              
493             =begin btest(extract)
494              
495             =end btest(extract)
496              
497             =cut
498              
499              
500             =pod
501              
502             =back
503              
504             =cut
505              
506             1;