File Coverage

blib/lib/StateML/Machine.pm
Criterion Covered Total %
statement 76 225 33.7
branch 9 74 12.1
condition 3 43 6.9
subroutine 15 36 41.6
pod 25 26 96.1
total 128 404 31.6


line stmt bran cond sub pod time code
1             package StateML::Machine;
2              
3             =head1 NAME
4              
5             StateML::Machine - a StateML state machine data structure
6              
7             =head1 DESCRIPTION
8              
9             Contains all events, arcs and states for a state machine.
10              
11             =head1 METHODS
12              
13             =over
14              
15             =cut
16              
17 3     3   6398 use strict ;
  3         5  
  3         88  
18 3     3   12 use Carp ;
  3         5  
  3         150  
19 3     3   1329 use StateML::Action ;
  3         17  
  3         65  
20 3     3   501 use StateML::Arc ;
  3         7  
  3         68  
21 3     3   1491 use StateML::Class ;
  3         6  
  3         72  
22 3     3   453 use StateML::Event ;
  3         6  
  3         66  
23 3     3   940 use StateML::State ;
  3         6  
  3         80  
24 3     3   17 use StateML::Utils qw( empty as_str );
  3         4  
  3         148  
25              
26 3     3   12 use base qw( StateML::Object ) ;
  3         41  
  3         9536  
27              
28             #use SelfTest ;
29              
30             =for testing
31             use Test ;
32             use StateML::Machine ;
33             plan tests => 0 ;
34              
35             =item new
36              
37             my $m = StateML::Machine->new ;
38              
39             =for testing
40             my $m = StateML::Machine->new(
41             EVENTS => [1,2],
42             ARCS => [1,2,3],
43             STATES => [1,2,3,4],
44             ) ;
45             ok( ref $m ) ;
46              
47             =cut
48              
49             sub new {
50 3     3 1 4413 my $proto = shift ;
51              
52 3         54 my $self = $proto->SUPER::new(
53             ACTIONS => [],
54             ARCS => [],
55             CLASSES => [],
56             EVENTS => [],
57             STATES => [],
58             OBJECTS => {}, ## All objects, indexed on id.
59             MODES => [],
60             ALL_STATE => StateML::State->new( ID => "#ALL", ORDER => -1 ),
61             @_, ## TODO: Error check the args.
62             ) ;
63              
64             ## Note that the #ALL state is not put in STATES.
65 3         162 my $all = $self->{ALL_STATE} ;
66 3         27 $all->machine( $self->{ID} ) ;
67 3         17 $all->_set_number( -1 ) ;
68 3         11 $all->{PARENT_ID} = $self->{ID} ;
69 3         10 $self->{OBJECTS}->{"#ALL"} = $all ;
70 3         24 $self->assert_valid ;
71 3         7 return $self ;
72             }
73              
74              
75             =item autogenerated_message
76              
77             Sets/gets a suitable warning message that can be placed in a template file.
78              
79             Use only [\w :./!,-] in this warning message and no newlines, tabs, or other
80             control codes.
81              
82             =cut
83              
84             sub autogenerated_message {
85 0     0 1 0 my $self = shift ;
86 0 0       0 if ( @_ ) {
87 0         0 my ( $message ) = @_ ;
88 0 0       0 Carp::confess unless defined $message;
89 0 0       0 if ( $message =~ /([^\w\t :.\\\/!,-])/ ) {
90 0         0 croak "Illegal characters ('$1') in message '$message'\n"
91             }
92 0         0 $self->{AUTOGENERATED_MESSAGE} = $message ;
93             }
94 0   0     0 return $self->{AUTOGENERATED_MESSAGE} || "AUTOGENERATED, DO NOT EDIT!!" ;
95             }
96              
97            
98              
99              
100             sub _number_states {
101             ## We do this lazily so that changes to a state's id or to the
102             ## list of states are always reflected in the numbers. That might
103             ## be overdesign, time will tell.
104 0     0   0 my $self = shift ;
105 0         0 my $i = 0 ;
106              
107             ## Number states startign at 1 so that 0 is left available for
108             ## initting or "unknown". #ALL is always -1.
109 0         0 $_->_set_number( ++$i )
110 0         0 for @{$self->{STATES}} ;
111             }
112              
113              
114             =item modes
115              
116             Set/get the list of modes that will be used to control what portions of
117             the document get parsed. This is used to conditionally control
118             inclusion of things like optional states or language-specific APIs.
119              
120             =cut
121              
122             sub modes {
123 0     0 1 0 my $self = shift ;
124 0 0       0 $self->{MODES} = [ @_ ] if @_ ;
125 0         0 return @{$self->{MODES}};
  0         0  
126             }
127              
128              
129             =item all_state
130              
131             Returns the "#ALL" state.
132              
133             =cut
134              
135 0     0 1 0 sub all_state { return shift()->{ALL_STATE} }
136              
137             =item states
138              
139             Returns a list of all states other than state #ALL.
140              
141             =for testing
142             ok( scalar $m->states, 4, "number of states" ) ;
143              
144             =cut
145              
146             sub states {
147 0     0 1 0 my $self = shift ;
148 0         0 $self->_number_states ;
149 0         0 return sort {
150 0         0 $a->number <=> $b->number
151 0         0 } @{$self->{STATES}} ;
152             }
153              
154              
155             =item raw_states
156              
157             Returns a list of all states including #ALL.
158              
159             =for testing
160             ok( scalar $m->states, 4, "number of states" ) ;
161              
162             =cut
163              
164             sub raw_states {
165 0     0 1 0 my $self = shift ;
166 0         0 $self->_number_states ;
167 0         0 return sort {
168 0         0 $a->number <=> $b->number
169 0         0 } @{$self->{STATES}}, $self->{ALL_STATE} ;
170             }
171              
172              
173             =item description
174              
175             Sets or gets a textual description of the machine
176              
177             =cut
178              
179             sub description {
180 0     0 1 0 my $self = shift ;
181 0 0       0 $self->{DESCRIPTION} = shift if @_ ;
182 0         0 return $self->{DESCRIPTION};
183             }
184              
185              
186             =item actions
187              
188             Returns a list of all actions.
189              
190             =for testing
191             ok( scalar $m->actions, 2, "number of actions" ) ;
192              
193             =cut
194              
195             sub actions {
196 0     0 1 0 my $self = shift ;
197 0         0 return @{$self->{ACTIONS}} ;
  0         0  
198             }
199              
200              
201             =item classes
202              
203             Returns a list of all classes
204              
205             =for testing
206             ok( scalar $m->classes, 2, "number of classes" ) ;
207              
208             =cut
209              
210             sub classes {
211 0     0 1 0 my $self = shift ;
212 0         0 return @{$self->{CLASSES}} ;
  0         0  
213             }
214              
215              
216             =item events
217              
218             Returns a list of all events.
219              
220             =for testing
221             ok( scalar $m->events, 2, "number of events" ) ;
222              
223             =cut
224              
225             sub events {
226 0     0 1 0 my $self = shift ;
227 0         0 return @{$self->{EVENTS}} ;
  0         0  
228             }
229              
230              
231             =item arcs
232              
233             Returns a list of all arcs.
234              
235             =for testing
236             ok( scalar $m->arcs, 3, "number of arcs" ) ;
237              
238             =cut
239              
240             sub arcs {
241 2     2 1 3 my $self = shift ;
242 2         3 return @{$self->{ARCS}} ;
  2         11  
243             }
244              
245              
246             =item preamble
247              
248             Returns the preamble code.
249              
250             =cut
251              
252             sub preamble {
253 0     0 1 0 my $self = shift ;
254 0         0 return $self->{PREAMBLE}->[0] ;
255             }
256              
257              
258             =item postamble
259              
260             Returns the postamble code.
261              
262             =cut
263              
264             sub postamble {
265 0     0 1 0 my $self = shift ;
266 0         0 return $self->{POSTAMBLE}->[0] ;
267             }
268              
269              
270             =item object_by_id
271              
272             my $object = $m->object_by_id( $id ) ;
273             my $object = $m->object_by_id( $id, $require_type ) ;
274              
275             Returns the state, event, or arc labelled $id or undef if one isn't found.
276              
277             If present, $required_type is used to make sure that the object requested
278             if of the indicated type.
279              
280             =cut
281              
282             sub object_by_id {
283 5     5 1 8 my $self = shift ;
284 5         11 my ( $id, $type ) = @_ ;
285              
286 5 50       14 return undef unless defined $id;
287              
288 5         8 my $obj ;
289 5 100       15 if ( exists $self->{OBJECTS}->{$id} ) {
290 4         11 $obj = $self->{OBJECTS}->{$id} ;
291 4 50 66     23 die "$id is not a $type"
292             if $type && ! $obj->isa( $type ) ;
293 4         50 return $obj ;
294             }
295 1         6 return undef ;
296             }
297              
298              
299             =item action_by_id
300              
301             Returns an action given it's id. Dies if $id refers to a non-state.
302              
303             =cut
304              
305             sub action_by_id {
306 0     0 1 0 my $self = shift ;
307 0         0 return $self->object_by_id( shift, "StateML::Action" ) ;
308             }
309              
310              
311             =item class_by_id
312              
313             Returns a class given it's id. Dies if $id refers to a non-class.
314              
315             In general this is not used because inheritance works across
316             all objects.
317              
318             =cut
319              
320             sub class_by_id {
321 0     0 1 0 my $self = shift ;
322 0         0 return $self->object_by_id( shift, "StateML::Class" ) ;
323             }
324              
325              
326             =item event_by_id
327              
328             Returns a event given it's id. Dies if $id refers to a non-event.
329              
330             =cut
331              
332             sub event_by_id {
333 2     2 1 3 my $self = shift ;
334 2         6 return $self->object_by_id( shift, "StateML::Event" ) ;
335             }
336              
337              
338             =item state_by_id
339              
340             Returns a state given it's id. Dies if $id refers to a non-state.
341              
342             =cut
343              
344             sub state_by_id {
345 0     0 1 0 my $self = shift ;
346 0         0 return $self->object_by_id( shift, "StateML::State" ) ;
347             }
348              
349              
350             =item add
351              
352             $m->add( $arc ) ;
353             $m->add( $class ) ;
354             $m->add( $event ) ;
355             $m->add( $state ) ;
356              
357             =cut
358              
359             sub add {
360 10     10 1 23 my $self = shift ;
361 10         24 for ( @_ ) {
362 10         46 my $id = $_->id ;
363 10 50 33     65 if ( exists $self->{OBJECTS}->{$id} || $id eq $self->{ID} ) {
364 0         0 my $new_type = ref $_ ;
365 0         0 my $old_type = ref $self->{OBJECTS}->{$id} ;
366 0         0 $old_type =~ s/^StateML::// ;
367 0         0 $new_type =~ s/^StateML::// ;
368 0 0       0 $new_type = $old_type eq $new_type ? "" : " (held by $new_type)" ;
369 0         0 croak "Can't add $old_type with duplicate ID '$id'$new_type.\n"
370             }
371 10         45 $_->machine( $self ) ;
372 10         28 $self->{OBJECTS}->{$_->id} = $_ ;
373 10         48 my $t = $_->type;
374 10 100       53 my $type = $t eq "CLASS" ? "${t}ES": "${t}S" ;
375 10         14 push @{$self->{$type}}, $_ ;
  10         42  
376             }
377             }
378              
379              
380             =item extract_output_machine
381              
382             my $om = $m->extract_output_machine( \@types ) ;
383              
384             Returns an output machine comprised of the events, arcs, and states
385             that match the \@types specified.
386              
387             =cut
388              
389             sub extract_output_machine {
390 0     0 1 0 my $self = shift ;
391 0         0 my $options = {@_} ;
392              
393 0         0 $options->{raw} = 1 ;
394              
395 0         0 my @events = $self->matching_events( $options ) ;
396 0 0       0 warn "no events found\n" unless @events ;
397              
398 0         0 $self->_number_states ;
399 0         0 my @arcs = map $self->arcs_for_event( $_, $options ), @events ;
400 0 0       0 warn "no arcs found\n" unless @arcs ;
401              
402 0         0 my @states = map $self->states_for_arc( $_, $options ), @arcs ;
403 0 0       0 warn "no states found\n" unless @states ;
404              
405             ## Remove #ALL and duplicate states.
406 0         0 @states = values %{{ map {
  0         0  
407 0         0 ( $_ => $_ )
408             } grep $_->id ne "#ALL", @states }} ;
409              
410 0         0 @states = sort { $a->number <=> $b->number } @states ;
  0         0  
411              
412 0         0 my $clone = $self->new(
413             ID => $self->{ID},
414             LOCATION => $self->{LOCATION},
415             ALL_STATE => $self->{ALL_STATE},
416             PREAMBLE => $self->{PREAMBLE},
417             POSTAMBLE => $self->{POSTAMBLE},
418             DESCRIPTION => $self->{DESCRIPTION},
419             ATTRS => $self->{ATTRS},
420 0         0 MODES => [ @{$self->{MODES}} ],
421             AUTOGENERATED_MESSAGE => $self->{AUTOGENERATED_MESSAGE},
422             ) ;
423              
424 0         0 $clone->add( @events, @states, @arcs, $self->classes, $self->actions ) ;
425              
426 0         0 return $clone ;
427             }
428              
429             =item matching_events
430              
431             my @events = $m->matching_events( types=>\@types ) ;
432             my @events = $m->matching_events( types=>[ "ui", "io" ] ) ;
433              
434             Gets all events that have type= attributes that match an entry in @types.
435             If no parameters are passed, all events are returned.
436              
437             Events with a type of "#ANY" or "#ALL" (case insensitive) will show up
438             in all filter settings. Passing "all", "any", "#all", or "#any" in the
439             typelist will cause all events to be returned.
440              
441             =cut
442              
443             sub matching_events {
444 0     0 1 0 my $self = shift ;
445 0 0 0     0 my $options = @_ && ref $_[-1] eq "HASH" ? pop : {} ;
446              
447 0         0 my $types = $options->{types} ;
448              
449 0 0 0     0 return @{$self->{EVENTS}} unless $types && @$types ;
  0         0  
450              
451 0         0 my %events ;
452             my @specs ;
453 0         0 my @not_specs ;
454              
455 0         0 for ( @$types ) {
456 0 0       0 if ( substr( $_, 0, 1 ) eq "!" ) {
457 0         0 push @not_specs, uc substr $_, 1 ;
458             }
459             else {
460 0         0 push @specs, uc $_ ;
461             }
462             }
463              
464 0         0 for ( @{$self->{EVENTS}} ) {
  0         0  
465 0         0 my $type_re = qr/^($_->{TYPE})$/i ;
466 0 0 0     0 $events{$_} = $_
      0        
      0        
      0        
467             if "#ALL" =~ $type_re
468             || "#ANY" =~ $type_re
469             || ( ( ! @specs && @not_specs ) || grep $_ =~ $type_re, @specs )
470             && ! grep( $_ =~ $type_re, @not_specs ) ;
471             }
472              
473 0         0 return values %events ;
474             }
475              
476              
477             =item arcs_for_event
478              
479             my @arcs = $m->arcs_for_event( $event ) ;
480              
481             Returns all arcs in the state machine for event $event.
482              
483             A arc is an edge in the state machine diagram.
484              
485             Unfolds arcs in state #ALL to be for all states.
486              
487             =cut
488              
489             sub arcs_for_event {
490 0     0 1 0 my $self = shift ;
491 0 0 0     0 my $options = @_ && ref $_[-1] eq "HASH" ? pop : {} ;
492 0         0 my ( $event ) = @_ ;
493              
494 0         0 my %arcs ;
495             my @arcs_for_all ;
496              
497             ## First, get all explicit ARCs, then inherit ARCs from #ALL if no
498             ## explicit ARCS.
499 0         0 my $uc_event_id = uc $event->id ;
500 0         0 for my $arc ( $self->arcs ) {
501 0 0       0 next unless uc $arc->event_id eq $uc_event_id ;
502 0 0       0 if ( uc $arc->from eq "#ALL" ) {
503 0         0 push @arcs_for_all, $arc ;
504             }
505              
506 0   0     0 $arcs{uc $arc->from . ($arc->guard || "" )} = $arc ;
507             }
508              
509 0 0 0     0 if ( exists $arcs{"#ALL"} && ! $options->{raw} ) {
510 0         0 delete $arcs{"#ALL"} ;
511 0         0 for my $arc ( @arcs_for_all ) {
512 0         0 for my $from_state ( $self->states ) {
513             ## #ALL arcs don't replace explicit arcs. TODO: We may add
514             ## a merge_with_overrides = "before" or "after" attr on #ALL
515             ## arcs to allow handlers from both arcs to be run.
516 0 0       0 next if exists $arcs{uc $from_state->id} ;
517 0 0       0 $arcs{uc $from_state->id} = StateML::Arc->new(
518             %$arc,
519             ID => $arc->id . "_" . $from_state->id,
520             FROM => $from_state->id,
521             TO => uc $arc->to eq "#ALL"
522             ? $from_state->id
523             : $arc->to,
524             DESCRIPTION => $arc->description,
525             ) ;
526             }
527             }
528             }
529              
530             ## Return results in a stable order, one that agrees with the state enum
531             ## and which perhaps is more likely to be easily optimizable by compilers.
532 0         0 map warn( $_->id ), grep( ! defined $_->number,
533 0         0 map( { ( $_->from_state, $_->to_state ) } values %arcs ) ) ;
534              
535 0         0 my @arcs = sort {
536 0         0 $a->from_state->number <=> $b->from_state->number
537             } values %arcs ;
538              
539 0         0 return @arcs ;
540             }
541              
542              
543             =item all_state_arcs_for_event
544              
545             my @arcs = $m->all_state_arcs_for_event( $event ) ;
546              
547             Returns all arcs in the state machine for event $event.
548              
549             A arc is an edge in the state machine diagram.
550              
551             =cut
552              
553             sub all_state_arc_for_event {
554 0     0 0 0 my $self = shift ;
555 0 0 0     0 my $options = @_ && ref $_[-1] eq "HASH" ? pop : {} ;
556 0         0 my ( $event ) = @_ ;
557              
558 0         0 my $uc_event_id = uc $event->id ;
559             ## Note that there can be only one arc for a given even in the #ALL state.
560 0         0 for my $arc ( $self->arcs ) {
561 0 0 0     0 next unless uc $arc->event_id eq $uc_event_id
562             && uc $arc->from eq "#ALL" ;
563 0         0 return $arc ;
564             }
565 0         0 return undef ;
566             }
567              
568              
569             =item states_by_id
570              
571             my %states_by_id = $m->states_by_id ;
572              
573             Returns a HASH ref of all states indexed by their id= attributes.
574              
575             =cut
576              
577             sub states_by_id {
578 0     0 1 0 my $self = shift ;
579              
580 0         0 $self->_number_states ;
581              
582             return {
583 0         0 map { ( $_->{ID} => $_ ) } @{$self->{STATES}}
  0         0  
  0         0  
584             } ;
585              
586             }
587              
588              
589             =item states_for_arc
590              
591             my @states = $m->states_for_arc( $arc ) ;
592              
593             Returns all states that appear as starting or ending points for $arc
594             other than the "#ALL" state. Will only return one state for loopbacks.
595              
596             =cut
597              
598             sub states_for_arc {
599 0     0 1 0 my $self = shift ;
600 0 0 0     0 my $options = @_ && ref $_[-1] eq "HASH" ? pop : {} ;
601 0         0 my ( $arc ) = @_ ;
602              
603 0         0 my %states ;
604              
605 0         0 $states{$arc->from} = $arc->from_state ;
606 0         0 $states{$arc->to} = $arc->to_state ;
607              
608 0         0 return values %states ;
609             }
610              
611              
612             =item assert_valid
613              
614             $m->assert_valid ;
615              
616             Dies if there are dangling references. The error message contains all
617             undefined states, events, etc.
618              
619             =cut
620              
621             sub assert_valid {
622 3     3 1 7 my $self = shift ;
623              
624 3         8 my @errors ;
625              
626             my %from_state_via_event ;
627 0         0 my %states_with_mult_arcs_same_event ;
628              
629 3         6 for my $arc ( @{$self->{ARCS}} ) {
  3         13  
630 0         0 my $unique_id = $arc->event_id;
631 0 0       0 $unique_id .= "[" . $arc->guard . "]" if defined $arc->guard;
632              
633 0         0 $DB::single = 1;
634 0 0       0 if ( empty $arc->from ) {
    0          
635 0         0 push @errors,
636             "no from state (",
637             as_str( $arc->from ),
638             ") in arc$arc->{LOCATION}\n"
639             }
640             elsif ( ! $self->state_by_id( $arc->from ) ) {
641 0         0 push @errors,
642             "unknown from state ",
643             as_str( $arc->from ),
644             " in arc$arc->{LOCATION}\n";
645             }
646             else {
647 0 0       0 $states_with_mult_arcs_same_event{$arc->from} = $unique_id
648             if $from_state_via_event{$arc->from,$unique_id};
649 0         0 $from_state_via_event{$arc->from,$unique_id} = $arc ;
650             }
651              
652 0 0       0 if ( empty $arc->to ) {
    0          
653 0         0 push @errors,
654             "no to state (", as_str( $arc->to ), ") in arc$arc->{LOCATION}\n";
655             }
656             elsif ( ! $self->state_by_id( $arc->to ) ) {
657 0         0 push @errors,
658             "unknown to state ",
659             as_str( $arc->to ),
660             " in arc$arc->{LOCATION}\n";
661             }
662              
663 0 0       0 if ( empty $arc->event_id ) {
    0          
664 0         0 push @errors,
665             "no event-id ",
666             as_str( $arc->event_id ),
667             " in arc$arc->{LOCATION}\n"
668             }
669             elsif ( ! $self->event_by_id( $arc->event_id ) ) {
670 0         0 push @errors,
671             "unknown event-id ",
672             as_str( $arc->event_id ),
673             " in arc$arc->{LOCATION}\n";
674             }
675             }
676              
677             ## TODO: Make this optional.
678 3         18 for ( sort keys %states_with_mult_arcs_same_event ) {
679 0         0 push @errors,
680             "multiple arcs exit from state $_ by event ",
681             $states_with_mult_arcs_same_event{$_},
682             "\n" ;
683             }
684              
685 3         13 my %dup_enum_ids ;
686             {
687 3         7 my %enum_ids ;
  3         6  
688 3         6 for ( values %{$self->{OBJECTS}} ) {
  3         13  
689 3 50       27 $dup_enum_ids{$_->enum_id} = $enum_ids{$_->enum_id}
690             if exists $enum_ids{$_->enum_id} ;
691 3         7 push @{$enum_ids{$_->enum_id}}, $_ ;
  3         13  
692             }
693             }
694              
695 3         11 for ( keys %dup_enum_ids ) {
696 0         0 warn
697             "multiple objects with the enum_id '$_': ",
698 0         0 join( " ", @{$dup_enum_ids{$_}} ),
699             "\n" ;
700             }
701              
702 3 50       10 die @errors if @errors ;
703              
704 3         12 return ;
705             }
706              
707             =back
708              
709             =head1 LIMITATIONS
710              
711             Alpha code. Ok test suite, but we may need to change things in
712             non-backward compatible ways.
713              
714             =head1 COPYRIGHT
715              
716             Copyright 2003, R. Barrie Slaymaker, Jr., All Rights Reserved
717              
718             =head1 LICENSE
719              
720             You may use this module under the terms of the BSD, Artistic, or GPL licenses,
721             any version.
722              
723             =head1 AUTHOR
724              
725             Barrie Slaymaker
726              
727             =cut
728              
729              
730             1 ;