File Coverage

blib/lib/Workflow/State.pm
Criterion Covered Total %
statement 149 172 86.6
branch 30 54 55.5
condition 2 9 22.2
subroutine 26 27 96.3
pod 13 13 100.0
total 220 275 80.0


line stmt bran cond sub pod time code
1             package Workflow::State;
2              
3 28     28   2322116 use warnings;
  28         59  
  28         2063  
4 28     28   161 use strict;
  28         51  
  28         817  
5 28     28   381 use v5.14.0;
  28         97  
6 28     28   166 use parent qw( Workflow::Base );
  28         54  
  28         183  
7 28     28   3499 use Workflow::Condition;
  28         61  
  28         377  
8 28     28   15384 use Workflow::Condition::Evaluate;
  28         113  
  28         234  
9 28     28   2342 use Workflow::Exception qw( workflow_error );
  28         63  
  28         1663  
10 28     28   174 use Exception::Class;
  28         68  
  28         302  
11 28     28   4095 use Workflow::Factory qw( FACTORY );
  28         71  
  28         222  
12              
13             $Workflow::State::VERSION = '2.09';
14              
15             my @FIELDS = qw( state description type );
16             my @INTERNAL = qw( _test_condition_count _factory _actions _conditions
17             _next_state );
18             __PACKAGE__->mk_accessors( @FIELDS, @INTERNAL );
19              
20              
21             ########################################
22             # PUBLIC
23              
24             sub get_conditions {
25 113     113 1 618 my ( $self, $action_name ) = @_;
26 113         494 $self->_contains_action_check($action_name);
27 113         1451 return @{ $self->_conditions->{$action_name} };
  113         394  
28             }
29              
30             sub get_action {
31 43     43 1 140 my ( $self, $wf, $action_name ) = @_;
32 43         206 my $common_config =
33             $self->_factory->get_action_config($wf, $action_name);
34 43         227 my $state_config = $self->_actions->{$action_name};
35 43         507 my $config = { %{$common_config}, %{$state_config} };
  43         246  
  43         367  
36 43         169 my $action_class = $common_config->{class};
37              
38 43         636 return $action_class->new( $wf, $config );
39             }
40              
41             sub contains_action {
42 192     192 1 463 my ( $self, $action_name ) = @_;
43 192         651 return $self->_actions->{$action_name};
44             }
45              
46             sub get_all_action_names {
47 25     25 1 67 my ($self) = @_;
48 25         52 return keys %{ $self->_actions };
  25         113  
49             }
50              
51             sub get_available_action_names {
52 25     25 1 8360 my ( $self, $wf, $group ) = @_;
53 25         115 my @all_actions = $self->get_all_action_names;
54 25         486 my @available_actions = ();
55              
56             # assuming that the user wants the _fresh_ list of available actions,
57             # we clear the condition cache before checking which ones are available
58 25         101 local $wf->{'_condition_result_cache'} = {};
59              
60 25         81 foreach my $action_name (@all_actions) {
61              
62 67 50       195 if ( $group ) {
63 0         0 my $action_config =
64             $self->_factory()->get_action_config( $wf, $action_name );
65 0 0 0     0 if ( defined $action_config->{group}
66             and $action_config->{group} ne $group ) {
67 0         0 next;
68             }
69             }
70              
71 67 100       255 if ( $self->is_action_available( $wf, $action_name ) ) {
72 39         127 push @available_actions, $action_name;
73             }
74             }
75 25         233 return @available_actions;
76             }
77              
78             sub is_action_available {
79 67     67 1 189 my ( $self, $wf, $action_name ) = @_;
80 67         280 return $self->evaluate_action( $wf, $action_name );
81             }
82              
83             sub clear_condition_cache {
84 0     0 1 0 my ($self) = @_;
85 0         0 return; # left for backward compatibility with 1.49
86             }
87              
88             sub evaluate_action {
89 110     110 1 296 my ( $self, $wf, $action_name ) = @_;
90 110         410 my $state = $self->state;
91              
92             # NOTE: this will throw an exception if C<$action_name> is not
93             # contained in this state, so there's no need to do it explicitly
94              
95 110         2863 my @conditions = $self->get_conditions($action_name);
96 110         1426 foreach my $condition (@conditions) {
97 69         340 my $condition_name = $condition->name;
98 69         1465 my $rv = Workflow::Condition->evaluate_condition($wf, $condition);
99 69 100       278 if (! $rv) {
100              
101 28 50       145 $self->log->is_debug
102             && $self->log->debug(
103             "No access to action '$action_name' in ",
104             "state '$state' because condition '$condition_name' failed");
105              
106 28         492 return $rv;
107             }
108             }
109              
110 82         317 return 1;
111             }
112              
113             sub get_next_state {
114 36     36 1 122 my ( $self, $action_name, $action_return ) = @_;
115 36         175 $self->_contains_action_check($action_name);
116 36         783 my $resulting_state = $self->_next_state->{$action_name};
117 36 50       564 return $resulting_state unless ( ref($resulting_state) eq 'HASH' );
118              
119 0 0       0 return %{$resulting_state} unless(defined $action_return);
  0         0  
120              
121 0         0 my $state = $self->state;
122 0 0       0 workflow_error "State->get_next_state was called with a non-scalar ",
123             "return value in state '$state' on action '$action_name'" if (ref $action_return ne '');
124              
125 0 0       0 return $resulting_state->{$action_return} if ($resulting_state->{$action_return});
126              
127 0 0       0 return $resulting_state->{'*'} if ($resulting_state->{'*'});
128              
129 0         0 workflow_error "State '$state' does not define a next state ",
130             "for a return value of '$action_return' and there is ",
131             "also no default state set.";
132              
133             }
134              
135             sub get_autorun_action_name {
136 5     5 1 19 my ( $self, $wf ) = @_;
137 5         29 my $state = $self->state;
138 5 50       101 unless ( $self->autorun ) {
139 0         0 workflow_error "State '$state' is not marked for automatic ",
140             "execution. If you want it to be run automatically ",
141             "set the 'autorun' property to 'yes'.";
142             }
143              
144 5         37 my @actions = $self->get_available_action_names($wf);
145 5         15 my $pre_error = "State '$state' should be automatically executed but";
146 5 50       23 if ( scalar @actions > 1 ) {
147 0         0 workflow_error "$pre_error there are multiple actions available ",
148             "for execution. Actions are: ", join ', ', @actions;
149             }
150 5 50       21 if ( scalar @actions == 0 ) {
151 0         0 workflow_error
152             "$pre_error there are no actions available for execution.";
153             }
154 5         30 $self->log->debug("Auto-running state '$state' with action '$actions[0]'");
155 5         46 return $actions[0];
156             }
157              
158             sub autorun {
159 231     231 1 595 my ( $self, $setting ) = @_;
160 231 100       909 if ( defined $setting ) {
161 162 100       1072 if ( $setting =~ /^(true|1|yes)$/i ) {
162 5         18 $self->{autorun} = 'yes';
163             } else {
164 157         470 $self->{autorun} = 'no';
165             }
166             }
167 231         875 return ( $self->{autorun} eq 'yes' );
168             }
169              
170             sub may_stop {
171 167     167 1 389 my ( $self, $setting ) = @_;
172 167 100       521 if ( defined $setting ) {
173 162 50       557 if ( $setting =~ /^(true|1|yes)$/i ) {
174 0         0 $self->{may_stop} = 'yes';
175             } else {
176 162         457 $self->{may_stop} = 'no';
177             }
178             }
179 167         404 return ( $self->{may_stop} eq 'yes' );
180             }
181              
182             ########################################
183             # INTERNAL
184              
185             sub init {
186 162     162 1 434 my ( $self, $config, $factory ) = @_;
187              
188             # Fallback for old style
189 162   33     452 $factory ||= FACTORY;
190 162         445 my $name = $config->{name};
191              
192 162         373 my $class = ref $self;
193              
194 162         545 $self->log->debug("Constructing '$class' object for state $name");
195              
196 162         19234 $self->state($name);
197 162         3011 $self->_factory($factory);
198 162         2046 $self->_actions( {} );
199 162         1887 $self->_conditions( {} );
200 162         2156 $self->_next_state( {} );
201              
202             # Note this is the workflow type.
203 162         2049 $self->type( $config->{type} );
204 162         2400 $self->description( $config->{description} );
205              
206 162 100       1956 if ( $config->{autorun} ) {
207 5         25 $self->autorun( $config->{autorun} );
208             } else {
209 157         554 $self->autorun('no');
210             }
211 162 50       487 if ( $config->{may_stop} ) {
212 0         0 $self->may_stop( $config->{may_stop} );
213             } else {
214 162         487 $self->may_stop('no');
215             }
216 162         370 foreach my $state_action_config ( @{ $config->{action} } ) {
  162         776  
217 165         797 my $action_name = $state_action_config->{name};
218 165         623 $self->log->debug("Adding action '$action_name' to '$class' '$name'");
219 165         920 $self->_add_action_config( $action_name, $state_action_config );
220             }
221             }
222              
223             sub _assign_next_state_from_array {
224 14     14   47 my ( $self, $action_name, $resulting ) = @_;
225 14         51 my $name = $self->state;
226 14         161 my @errors = ();
227 14         40 my %new_resulting = ();
228 14         33 foreach my $map ( @{$resulting} ) {
  14         49  
229 28 50 33     233 if ( not $map->{state} or not defined $map->{return} ) {
    50          
230 0         0 push @errors,
231             "Must have both 'state' ($map->{state}) and 'return' "
232             . "($map->{return}) keys defined.";
233             } elsif ( $new_resulting{ $map->{return} } ) {
234 0         0 push @errors, "The 'return' value ($map->{return}) must be "
235             . "unique among the resulting states.";
236             } else {
237 28         134 $new_resulting{ $map->{return} } = $map->{state};
238             }
239             }
240 14 50       68 if ( scalar @errors ) {
241 0         0 workflow_error "Errors found assigning 'resulting_state' to ",
242             "action '$action_name' in state '$name': ", join '; ', @errors;
243             }
244 14         101 $self->log->debug( "Assigned multiple resulting states in '$name' and ",
245             "action '$action_name' from array ok" );
246 14         76 return \%new_resulting;
247             }
248              
249             sub _create_next_state {
250 165     165   378 my ( $self, $action_name, $resulting ) = @_;
251              
252 165 100       568 if ( my $resulting_type = ref $resulting ) {
253 14 50       67 if ( $resulting_type eq 'ARRAY' ) {
254 14         167 $resulting
255             = $self->_assign_next_state_from_array( $action_name,
256             $resulting );
257             }
258             }
259              
260 165         633 return $resulting;
261             }
262              
263             sub _add_action_config {
264 165     165   436 my ( $self, $action_name, $action_config ) = @_;
265 165         581 my $state = $self->state;
266 165 50       2374 unless ( $action_config->{resulting_state} ) {
267 0         0 my $no_change_value = Workflow->NO_CHANGE_VALUE;
268 0         0 workflow_error "Action '$action_name' in state '$state' does not ",
269             "have the key 'resulting_state' defined. This key ",
270             "is required -- if you do not want the state to ",
271             "change, use the value '$no_change_value'.";
272             }
273             # Copy the action config,
274             # so we can delete keys consumed by the state below
275 165         1022 my $copied_config = { %$action_config };
276 165         467 my $resulting_state = delete $copied_config->{resulting_state};
277 165         374 my $condition = delete $copied_config->{condition};
278              
279             # Removes 'resulting_state' key from action_config
280 165         529 $self->_next_state->{$action_name} =
281             $self->_create_next_state( $action_name, $resulting_state );
282              
283             # Removes 'condition' key from action_config
284 165         2216 $self->_conditions->{$action_name} = [
285             $self->_create_condition_objects( $action_name, $condition )
286             ];
287              
288 165         2181 $self->_actions->{$action_name} = $copied_config;
289             }
290              
291             sub _create_condition_objects {
292 165     165   363 my ( $self, $action_name, $action_conditions ) = @_;
293 165         671 my @conditions = $self->normalize_array( $action_conditions );
294 165         310 my @condition_objects = ();
295 165         287 my $count = 1;
296 165         385 foreach my $condition_info (@conditions) {
297              
298             # Special case: a 'test' denotes our 'evaluate' condition
299 98 100       424 if ( $condition_info->{test} ) {
300 20         92 my $state = $self->state();
301             push @condition_objects,
302             Workflow::Condition::Evaluate->new(
303             { name => "_$state\_$action_name\_condition\_$count",
304             class => 'Workflow::Condition::Evaluate',
305             test => $condition_info->{test},
306             }
307 20         578 );
308 20         92 $count++;
309             } else {
310 78         238 $self->log->info(
311             "Fetching condition '$condition_info->{name}'");
312             push @condition_objects,
313             $self->_factory()
314 78         412 ->get_condition( $condition_info->{name}, $self->type() );
315             }
316             }
317 165         775 return @condition_objects;
318             }
319              
320             sub _contains_action_check {
321 149     149   348 my ( $self, $action_name ) = @_;
322 149 50       465 unless ( $self->contains_action($action_name) ) {
323 0           workflow_error "State '", $self->state, "' does not contain ",
324             "action '$action_name'";
325             }
326             }
327              
328             1;
329              
330             __END__
331              
332             =pod
333              
334             =head1 NAME
335              
336             Workflow::State - Information about an individual state in a workflow
337              
338             =head1 VERSION
339              
340             This documentation describes version 2.09 of this package
341              
342             =head1 SYNOPSIS
343              
344             # This is an internal object...
345             state:
346             - name: Start
347             description: |- # optional
348             My state documentation
349             action:
350             - ...
351             resulting_state: Progress
352             ...
353             - name: Progress
354             description: I am in progress
355             action:
356             - ...
357             resulting_state:
358             0: 'Needs Affirmation'
359             1: 'Approved'
360             *: 'Needs More Info'
361             - name: Approved
362             autorun: yes
363             action:
364             - ...
365             resulting_state: Completed
366              
367             =head1 DESCRIPTION
368              
369             Each L<Workflow::State> object represents a state in a workflow. Each
370             state can report its name, description and all available
371             actions. Given the name of an action it can also report what
372             conditions are attached to the action and what state will result from
373             the action (the 'resulting state').
374              
375             =head2 Resulting State
376              
377             The resulting state is action-dependent. For instance, in the
378             following example you can perform two actions from the state 'Ticket
379             Created' -- 'add comment' and 'edit issue':
380              
381             state:
382             - name: Ticket Created
383             action:
384             - name: add comment
385             resulting_state: NOCHANGE
386             - name: edit issue
387             resulting_state: Ticket In Progress
388              
389             If you execute 'add comment' the new state of the workflow will be the
390             same ('NOCHANGE' is a special state). But if you execute 'edit issue'
391             the new state will be 'Ticket In Progress'.
392              
393             You can also have multiple return states for a single action. The one
394             chosen by the workflow system will depend on what the action
395             returns. For instance we might have something like:
396              
397             state:
398             - name: create user
399             action:
400             - name: create
401             resulting_state:
402             admin: Assign as Admin
403             helpdesk: Assign as Helpdesk
404             *: Assign as Luser
405              
406             So if we execute 'create' the workflow will be in one of three states:
407             'Assign as Admin' if the return value of the 'create' action is
408             'admin', 'Assign as Helpdesk' if the return is 'helpdesk', and 'Assign
409             as Luser' if the return is anything else.
410              
411             =head2 Action availability
412              
413             A state can have multiple actions associated with it, demonstrated in the
414             first example under L</Resulting State>. The set of I<available> actions is
415             a subset of all I<associated> actions: those actions for which none of the
416             associated conditions fail their check.
417              
418             state:
419             - name: create
420             resulting_state:
421             ... (resulting states) ...
422             condition:
423             - name: can_create_users
424              
425              
426             =head2 Autorun State
427              
428             You can also indicate that the state should be automatically executed
429             when the workflow enters it using the 'autorun' property. Note the
430             slight change in terminology -- typically we talk about executing an
431             action, not a state. But we can use both here because an automatically
432             run state requires that one and only one action is I<available> for
433             running. That doesn't mean a state contains only one action. It just
434             means that only one action is I<available> when the state is entered. For
435             example, you might have two actions with mutually exclusive conditions
436             within the autorun state.
437              
438             =head3 Stoppable autorun states
439              
440             If no action or more than one action is I<available> at the time the
441             workflow enters an autorun state, Workflow can't continue execution.
442             If this is isn't a problem, a state may be marked with C<may_stop="yes">:
443              
444              
445             state:
446             - name: Approved
447             autorun: yes
448             may_stop: yes
449             action:
450             - name: Archive
451             resulting_state: Completed
452             condition:
453             - name: allowed_automatic_archival
454              
455              
456             However, in case the state isn't marked C<may_stop: yes>, Workflow will
457             throw a C<workflow_error> indicating an autorun problem.
458              
459              
460             =head1 PUBLIC METHODS
461              
462             =head3 get_conditions( $action_name )
463              
464             Returns a list of L<Workflow::Condition> objects for action
465             C<$action_name>. Throws exception if object does not contain
466             C<$action_name> at all.
467              
468             =head3 get_action( $workflow, $action_name )
469              
470             Returns an L<Workflow::Action> instance initialized using both the
471             global configuration provided to the named action in the "action
472             configuration" provided to the factory as well as any configuration
473             specified as part of the listing of actions in the state of the
474             workflow declaration.
475              
476             =head3 contains_action( $action_name )
477              
478             Returns true if this state contains action C<$action_name>, false if
479             not.
480              
481             =head3 is_action_available( $workflow, $action_name )
482              
483             Returns true if C<$action_name> is contained within this state B<and>
484             it matches any conditions attached to it, using the data in the
485             context of the C<$workflow> to do the checks.
486              
487             =head3 evaluate_action( $workflow, $action_name )
488              
489             Throws exception if action C<$action_name> is either not contained in
490             this state or if it does not pass any of the attached conditions,
491             using the data in the context of C<$workflow> to do the checks.
492              
493             =head3 get_all_action_names()
494              
495             Returns list of all action names available in this state.
496              
497             =head3 get_available_action_names( $workflow, $group )
498              
499             Returns all actions names that are available given the data in
500             C<$workflow>. Each action name returned will return true from
501             B<is_action_available()>.
502             $group is optional parameter. If it is set, additional check for group
503             membership will be performed.
504              
505             =head3 get_next_state( $action_name, [ $action_return ] )
506              
507             Returns the state(s) that will result if action C<$action_name>
508             is executed. If you've specified multiple return states in the
509             configuration then you need to specify the C<$action_return>,
510             otherwise we return a hash with action return values as the keys and
511             the action names as the values.
512              
513             =head3 get_autorun_action_name( $workflow )
514              
515             Retrieve the action name to be autorun for this state. If the state
516             does not have the 'autorun' property enabled this throws an
517             exception. It also throws an exception if there are multiple actions
518             available or if there are no actions available.
519              
520             Returns name of action to be used for autorunning the state.
521              
522             =head3 clear_condition_cache ( )
523              
524             Deprecated, kept for 2.06 compatibility.
525              
526             Used to empties the condition result cache for a given state.
527              
528             =head1 PROPERTIES
529              
530             All property methods act as a getter and setter. For example:
531              
532             my $state_name = $state->state;
533             $state->state( 'some name' );
534              
535             B<state>
536              
537             Name of this state (required).
538              
539             B<description>
540              
541             Description of this state (optional).
542              
543             =head3 autorun
544              
545             Returns true if the state should be automatically run, false if
546             not. To set to true the property value should be 'yes', 'true' or 1.
547              
548             =head3 may_stop
549              
550             Returns true if the state may stop automatic execution silently, false
551             if not. To set to true the property value should be 'yes', 'true' or 1.
552              
553             =head1 INTERNAL METHODS
554              
555             =head3 init( $config )
556              
557             Assigns 'state', 'description', 'autorun' and 'may_stop' properties from
558             C<$config>. Also assigns configuration for all actions in the state,
559             performing some sanity checks like ensuring every action has a
560             'resulting_state' key.
561              
562             =head1 SEE ALSO
563              
564             =over
565              
566             =item * L<Workflow>
567              
568             =item * L<Workflow::Condition>
569              
570             =item * L<Workflow::Factory>
571              
572             =back
573              
574             =head1 COPYRIGHT
575              
576             Copyright (c) 2003-2021 Chris Winters. All rights reserved.
577              
578             This library is free software; you can redistribute it and/or modify
579             it under the same terms as Perl itself.
580              
581             Please see the F<LICENSE>
582              
583             =head1 AUTHORS
584              
585             Please see L<Workflow>
586              
587             =cut