• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jonasbn / perl-workflow / 5776220845

pending completion
5776220845

push

github

web-flow
Merge pull request #193 from jonasbn/validator-as-interface

Document that Workflow::Validator is an interface contract

3 of 3 new or added lines in 3 files covered. (100.0%)

1345 of 1457 relevant lines covered (92.31%)

48.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.27
/lib/Workflow/State.pm
1
package Workflow::State;
2

3
use warnings;
23✔
4
use strict;
23✔
5
use v5.14.0;
23✔
6
use parent qw( Workflow::Base );
23✔
7
use Workflow::Condition;
23✔
8
use Workflow::Condition::Evaluate;
23✔
9
use Workflow::Exception qw( workflow_error );
23✔
10
use Exception::Class;
23✔
11
use Workflow::Factory qw( FACTORY );
23✔
12

13
$Workflow::State::VERSION = '1.57';
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
    my ( $self, $action_name ) = @_;
140✔
26
    $self->_contains_action_check($action_name);
140✔
27
    return @{ $self->_conditions->{$action_name} };
140✔
28
}
29

30
sub get_action {
31
    my ( $self, $wf, $action_name ) = @_;
61✔
32
    my $common_config =
61✔
33
        $self->_factory->get_action_config($wf, $action_name);
34
    my $state_config  = $self->_actions->{$action_name};
61✔
35
    my $config        = { %{$common_config}, %{$state_config} };
61✔
36
    my $action_class  = $common_config->{class};
61✔
37

38
    return $action_class->new( $wf, $config );
61✔
39
}
40

41
sub contains_action {
42
    my ( $self, $action_name ) = @_;
255✔
43
    return $self->_actions->{$action_name};
255✔
44
}
45

46
sub get_all_action_names {
47
    my ($self) = @_;
29✔
48
    return keys %{ $self->_actions };
29✔
49
}
50

51
sub get_available_action_names {
52
    my ( $self, $wf, $group ) = @_;
29✔
53
    my @all_actions       = $self->get_all_action_names;
29✔
54
    my @available_actions = ();
29✔
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
    local $wf->{'_condition_result_cache'} = {};
29✔
59

60
    foreach my $action_name (@all_actions) {
29✔
61

62
        if ( $group ) {
76✔
63
            my $action_config =
64
                $self->_factory()->get_action_config( $wf, $action_name );
65
            if ( defined $action_config->{group}
66
                 and $action_config->{group} ne $group ) {
67
                next;
68
            }
69
        }
70

71
        if ( $self->is_action_available( $wf, $action_name ) ) {
76✔
72
            push @available_actions, $action_name;
73
        }
74
    }
75
    return @available_actions;
29✔
76
}
77

78
sub is_action_available {
79
    my ( $self, $wf, $action_name ) = @_;
76✔
80
    return $self->evaluate_action( $wf, $action_name );
76✔
81
}
82

83
sub clear_condition_cache {
84
    my ($self) = @_;
×
85
    return; # left for backward compatibility with 1.49
×
86
}
87

88
sub evaluate_action {
89
    my ( $self, $wf, $action_name ) = @_;
137✔
90
    my $state = $self->state;
137✔
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
    my @conditions = $self->get_conditions($action_name);
137✔
96
    foreach my $condition (@conditions) {
137✔
97
        my $condition_name = $condition->name;
84✔
98
        my $rv = Workflow::Condition->evaluate_condition($wf, $condition_name);
84✔
99
        if (! $rv) {
84✔
100

101
            $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
            return $rv;
107
        }
108
    }
109

110
    return 1;
104✔
111
}
112

113
sub get_next_state {
114
    my ( $self, $action_name, $action_return ) = @_;
54✔
115
    $self->_contains_action_check($action_name);
54✔
116
    my $resulting_state = $self->_next_state->{$action_name};
54✔
117
    return $resulting_state unless ( ref($resulting_state) eq 'HASH' );
54✔
118

119
    if ( defined $action_return ) {
×
120

121
       # TODO: Throw exception if $action_return not found and no '*' defined?
122
        return $resulting_state->{$action_return} || $resulting_state->{'*'};
123
    } else {
124
        return %{$resulting_state};
125
    }
126
}
127

128
sub get_autorun_action_name {
129
    my ( $self, $wf ) = @_;
9✔
130
    my $state = $self->state;
9✔
131
    unless ( $self->autorun ) {
9✔
132
        workflow_error "State '$state' is not marked for automatic ",
133
            "execution. If you want it to be run automatically ",
134
            "set the 'autorun' property to 'yes'.";
135
    }
136

137
    my @actions   = $self->get_available_action_names($wf);
9✔
138
    my $pre_error = "State '$state' should be automatically executed but";
9✔
139
    if ( scalar @actions > 1 ) {
9✔
140
        workflow_error "$pre_error there are multiple actions available ",
141
            "for execution. Actions are: ", join ', ', @actions;
142
    }
143
    if ( scalar @actions == 0 ) {
9✔
144
        workflow_error
145
            "$pre_error there are no actions available for execution.";
146
    }
147
    $self->log->debug("Auto-running state '$state' with action '$actions[0]'");
9✔
148
    return $actions[0];
9✔
149
}
150

151
sub autorun {
152
    my ( $self, $setting ) = @_;
263✔
153
    if ( defined $setting ) {
263✔
154
        if ( $setting =~ /^(true|1|yes)$/i ) {
155
            $self->{autorun} = 'yes';
156
        } else {
157
            $self->{autorun} = 'no';
158
        }
159
    }
160
    return ( $self->{autorun} eq 'yes' );
263✔
161
}
162

163
sub may_stop {
164
    my ( $self, $setting ) = @_;
185✔
165
    if ( defined $setting ) {
185✔
166
        if ( $setting =~ /^(true|1|yes)$/i ) {
167
            $self->{may_stop} = 'yes';
168
        } else {
169
            $self->{may_stop} = 'no';
170
        }
171
    }
172
    return ( $self->{may_stop} eq 'yes' );
185✔
173
}
174

175
########################################
176
# INTERNAL
177

178
sub init {
179
    my ( $self, $config, $factory ) = @_;
176✔
180

181
    # Fallback for old style
182
    $factory ||= FACTORY;
176✔
183
    my $name = $config->{name};
176✔
184

185
    my $class = ref $self;
176✔
186

187
    $self->log->debug("Constructing '$class' object for state $name");
176✔
188

189
    $self->state($name);
176✔
190
    $self->_factory($factory);
176✔
191
    $self->_actions( {} );
176✔
192
    $self->_conditions( {} );
176✔
193
    $self->_next_state( {} );
176✔
194

195
    # Note this is the workflow type.
196
    $self->type( $config->{type} );
176✔
197
    $self->description( $config->{description} );
176✔
198

199
    if ( $config->{autorun} ) {
176✔
200
        $self->autorun( $config->{autorun} );
201
    } else {
202
        $self->autorun('no');
203
    }
204
    if ( $config->{may_stop} ) {
176✔
205
        $self->may_stop( $config->{may_stop} );
206
    } else {
207
        $self->may_stop('no');
208
    }
209
    foreach my $state_action_config ( @{ $config->{action} } ) {
176✔
210
        my $action_name = $state_action_config->{name};
190✔
211
        $self->log->debug("Adding action '$action_name' to '$class' '$name'");
190✔
212
        $self->_add_action_config( $action_name, $state_action_config );
190✔
213
    }
214
}
215

216
sub _assign_next_state_from_array {
217
    my ( $self, $action_name, $resulting ) = @_;
15✔
218
    my $name          = $self->state;
15✔
219
    my @errors        = ();
15✔
220
    my %new_resulting = ();
15✔
221
    foreach my $map ( @{$resulting} ) {
15✔
222
        if ( not $map->{state} or not defined $map->{return} ) {
30✔
223
            push @errors,
224
                "Must have both 'state' ($map->{state}) and 'return' "
225
                . "($map->{return}) keys defined.";
226
        } elsif ( $new_resulting{ $map->{return} } ) {
227
            push @errors, "The 'return' value ($map->{return}) must be "
228
                . "unique among the resulting states.";
229
        } else {
230
            $new_resulting{ $map->{return} } = $map->{state};
231
        }
232
    }
233
    if ( scalar @errors ) {
15✔
234
        workflow_error "Errors found assigning 'resulting_state' to ",
235
            "action '$action_name' in state '$name': ", join '; ', @errors;
236
    }
237
    $self->log->debug( "Assigned multiple resulting states in '$name' and ",
15✔
238
                       "action '$action_name' from array ok" );
239
    return \%new_resulting;
15✔
240
}
241

242
sub _create_next_state {
243
    my ( $self, $action_name, $resulting ) = @_;
190✔
244

245
    if ( my $resulting_type = ref $resulting ) {
190✔
246
        if ( $resulting_type eq 'ARRAY' ) {
247
            $resulting
248
                = $self->_assign_next_state_from_array( $action_name,
249
                                                        $resulting );
250
        }
251
    }
252

253
    return $resulting;
190✔
254
}
255

256
sub _add_action_config {
257
    my ( $self, $action_name, $action_config ) = @_;
190✔
258
    my $state = $self->state;
190✔
259
    unless ( $action_config->{resulting_state} ) {
190✔
260
        my $no_change_value = Workflow->NO_CHANGE_VALUE;
261
        workflow_error "Action '$action_name' in state '$state' does not ",
262
            "have the key 'resulting_state' defined. This key ",
263
            "is required -- if you do not want the state to ",
264
            "change, use the value '$no_change_value'.";
265
    }
266
    # Copy the action config,
267
    # so we can delete keys consumed by the state below
268
    my $copied_config   = { %$action_config };
190✔
269
    my $resulting_state = delete $copied_config->{resulting_state};
190✔
270
    my $condition       = delete $copied_config->{condition};
190✔
271

272
    # Removes 'resulting_state' key from action_config
273
    $self->_next_state->{$action_name} =
190✔
274
        $self->_create_next_state( $action_name, $resulting_state );
275

276
    # Removes 'condition' key from action_config
277
    $self->_conditions->{$action_name} = [
190✔
278
        $self->_create_condition_objects( $action_name, $condition )
279
        ];
280

281
    $self->_actions->{$action_name} = $copied_config;
190✔
282
}
283

284
sub _create_condition_objects {
285
    my ( $self, $action_name, $action_conditions ) = @_;
190✔
286
    my @conditions = $self->normalize_array( $action_conditions );
190✔
287
    my @condition_objects = ();
190✔
288
    my $count             = 1;
190✔
289
    foreach my $condition_info (@conditions) {
190✔
290

291
        # Special case: a 'test' denotes our 'evaluate' condition
292
        if ( $condition_info->{test} ) {
112✔
293
            my $state  = $self->state();
294
            push @condition_objects,
295
                Workflow::Condition::Evaluate->new(
296
                {   name  => "_$state\_$action_name\_condition\_$count",
297
                    class => 'Workflow::Condition::Evaluate',
298
                    test  => $condition_info->{test},
299
                }
300
                );
301
            $count++;
302
        } else {
303
            $self->log->info(
304
                "Fetching condition '$condition_info->{name}'");
305
            push @condition_objects,
306
                $self->_factory()
307
                ->get_condition( $condition_info->{name}, $self->type() );
308
        }
309
    }
310
    return @condition_objects;
190✔
311
}
312

313
sub _contains_action_check {
314
    my ( $self, $action_name ) = @_;
194✔
315
    unless ( $self->contains_action($action_name) ) {
194✔
316
        workflow_error "State '", $self->state, "' does not contain ",
317
            "action '$action_name'";
318
    }
319
}
320

321
1;
322

323
__END__
324

325
=pod
326

327
=head1 NAME
328

329
Workflow::State - Information about an individual state in a workflow
330

331
=head1 VERSION
332

333
This documentation describes version 1.57 of this package
334

335
=head1 SYNOPSIS
336

337
 # This is an internal object...
338
 <workflow...>
339
   <state name="Start">
340
     <description>My state documentation</description> <!-- optional -->
341
     <action ... resulting_state="Progress" />
342
   </state>
343
      ...
344
   <state name="Progress" description="I am in progress">
345
     <action ... >
346
        <resulting_state return="0" state="Needs Affirmation" />
347
        <resulting_state return="1" state="Approved" />
348
        <resulting_state return="*" state="Needs More Info" />
349
     </action>
350
   </state>
351
      ...
352
   <state name="Approved" autorun="yes">
353
     <action ... resulting_state="Completed" />
354
      ...
355

356
=head1 DESCRIPTION
357

358
Each L<Workflow::State> object represents a state in a workflow. Each
359
state can report its name, description and all available
360
actions. Given the name of an action it can also report what
361
conditions are attached to the action and what state will result from
362
the action (the 'resulting state').
363

364
=head2 Resulting State
365

366
The resulting state is action-dependent. For instance, in the
367
following example you can perform two actions from the state 'Ticket
368
Created' -- 'add comment' and 'edit issue':
369

370
  <state name="Ticket Created">
371
     <action name="add comment"
372
             resulting_state="NOCHANGE" />
373
     <action name="edit issue"
374
             resulting_state="Ticket In Progress" />
375
   </state>
376

377
If you execute 'add comment' the new state of the workflow will be the
378
same ('NOCHANGE' is a special state). But if you execute 'edit issue'
379
the new state will be 'Ticket In Progress'.
380

381
You can also have multiple return states for a single action. The one
382
chosen by the workflow system will depend on what the action
383
returns. For instance we might have something like:
384

385
  <state name="create user">
386
     <action name="create">
387
         <resulting_state return="admin"    state="Assign as Admin" />
388
         <resulting_state return="helpdesk" state="Assign as Helpdesk" />
389
         <resulting_state return="*"        state="Assign as Luser" />
390
     </action>
391
  </state>
392

393
So if we execute 'create' the workflow will be in one of three states:
394
'Assign as Admin' if the return value of the 'create' action is
395
'admin', 'Assign as Helpdesk' if the return is 'helpdesk', and 'Assign
396
as Luser' if the return is anything else.
397

398
=head2 Action availability
399

400
A state can have multiple actions associated with it, demonstrated in the
401
first example under L</Resulting State>. The set of I<available> actions is
402
a subset of all I<associated> actions: those actions for which none of the
403
associated conditions fail their check.
404

405
  <state name="create user">
406
     <action name="create">
407
         ... (resulting_states) ...
408
         <condition name="can_create_users" />
409
     </action>
410
  </state>
411

412

413

414
=head2 Autorun State
415

416
You can also indicate that the state should be automatically executed
417
when the workflow enters it using the 'autorun' property. Note the
418
slight change in terminology -- typically we talk about executing an
419
action, not a state. But we can use both here because an automatically
420
run state requires that one and only one action is I<available> for
421
running. That doesn't mean a state contains only one action. It just
422
means that only one action is I<available> when the state is entered. For
423
example, you might have two actions with mutually exclusive conditions
424
within the autorun state.
425

426
=head3 Stoppable autorun states
427

428
If no action or more than one action is I<available> at the time the
429
workflow enters an autorun state, Workflow can't continue execution.
430
If this is isn't a problem, a state may be marked with C<may_stop="yes">:
431

432

433
   <state name="Approved" autorun="yes" may_stop="yes">
434
     <action name="Archive" resulting_state="Completed" />
435
        <condition name="allowed_automatic_archival" />
436
     </action>
437
  </state>
438

439

440
However, in case the state isn't marked C<may_stop="yes">, Workflow will
441
throw a C<workflow_error> indicating an autorun problem.
442

443

444
=head1 PUBLIC METHODS
445

446
=head3 get_conditions( $action_name )
447

448
Returns a list of L<Workflow::Condition> objects for action
449
C<$action_name>. Throws exception if object does not contain
450
C<$action_name> at all.
451

452
=head3 get_action( $workflow, $action_name )
453

454
Returns an L<Workflow::Action> instance initialized using both the
455
global configuration provided to the named action in the "action
456
configuration" provided to the factory as well as any configuration
457
specified as part of the listing of actions in the state of the
458
workflow declaration.
459

460
=head3 contains_action( $action_name )
461

462
Returns true if this state contains action C<$action_name>, false if
463
not.
464

465
=head3 is_action_available( $workflow, $action_name )
466

467
Returns true if C<$action_name> is contained within this state B<and>
468
it matches any conditions attached to it, using the data in the
469
context of the C<$workflow> to do the checks.
470

471
=head3 evaluate_action( $workflow, $action_name )
472

473
Throws exception if action C<$action_name> is either not contained in
474
this state or if it does not pass any of the attached conditions,
475
using the data in the context of C<$workflow> to do the checks.
476

477
=head3 get_all_action_names()
478

479
Returns list of all action names available in this state.
480

481
=head3 get_available_action_names( $workflow, $group )
482

483
Returns all actions names that are available given the data in
484
C<$workflow>. Each action name returned will return true from
485
B<is_action_available()>.
486
$group is optional parameter. If it is set, additional check for group
487
membership will be performed.
488

489
=head3 get_next_state( $action_name, [ $action_return ] )
490

491
Returns the state(s) that will result if action C<$action_name>
492
is executed. If you've specified multiple return states in the
493
configuration then you need to specify the C<$action_return>,
494
otherwise we return a hash with action return values as the keys and
495
the action names as the values.
496

497
=head3 get_autorun_action_name( $workflow )
498

499
Retrieve the action name to be autorun for this state. If the state
500
does not have the 'autorun' property enabled this throws an
501
exception. It also throws an exception if there are multiple actions
502
available or if there are no actions available.
503

504
Returns name of action to be used for autorunning the state.
505

506
=head3 clear_condition_cache ( )
507

508
Deprecated, kept for 1.57 compatibility.
509

510
Used to empties the condition result cache for a given state.
511

512
=head1 PROPERTIES
513

514
All property methods act as a getter and setter. For example:
515

516
 my $state_name = $state->state;
517
 $state->state( 'some name' );
518

519
B<state>
520

521
Name of this state (required).
522

523
B<description>
524

525
Description of this state (optional).
526

527
=head3 autorun
528

529
Returns true if the state should be automatically run, false if
530
not. To set to true the property value should be 'yes', 'true' or 1.
531

532
=head3 may_stop
533

534
Returns true if the state may stop automatic execution silently, false
535
if not. To set to true the property value should be 'yes', 'true' or 1.
536

537
=head1 INTERNAL METHODS
538

539
=head3 init( $config )
540

541
Assigns 'state', 'description', 'autorun' and 'may_stop' properties from
542
C<$config>. Also assigns configuration for all actions in the state,
543
performing some sanity checks like ensuring every action has a
544
'resulting_state' key.
545

546
=head1 SEE ALSO
547

548
=over
549

550
=item * L<Workflow>
551

552
=item * L<Workflow::Condition>
553

554
=item * L<Workflow::Factory>
555

556
=back
557

558
=head1 COPYRIGHT
559

560
Copyright (c) 2003-2021 Chris Winters. All rights reserved.
561

562
This library is free software; you can redistribute it and/or modify
563
it under the same terms as Perl itself.
564

565
Please see the F<LICENSE>
566

567
=head1 AUTHORS
568

569
Please see L<Workflow>
570

571
=cut
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc