File Coverage

blib/lib/Workflow/Action.pm
Criterion Covered Total %
statement 90 96 93.7
branch 6 10 60.0
condition 1 2 50.0
subroutine 16 17 94.1
pod 9 9 100.0
total 122 134 91.0


line stmt bran cond sub pod time code
1             package Workflow::Action;
2              
3             # Note: we may implement a separate event mechanism so that actions
4             # can trigger other code (to read observations from database?)
5              
6 28     28   2339979 use warnings;
  28         53  
  28         2007  
7 28     28   174 use strict;
  28         58  
  28         741  
8 28     28   318 use v5.14.0;
  28         99  
9 28     28   200 use parent qw( Workflow::Base );
  28         57  
  28         239  
10 28     28   18358 use Workflow::InputField;
  28         96  
  28         276  
11 28     28   17481 use Workflow::Validator::HasRequiredField;
  28         98  
  28         340  
12 28     28   5088 use Workflow::Factory qw( FACTORY );
  28         80  
  28         162  
13 28     28   1466 use Carp qw(croak);
  28         82  
  28         45174  
14              
15             $Workflow::Action::VERSION = '2.09';
16              
17             my @PROPS = qw( name class description group );
18             my @INTERNAL = qw( _factory );
19             __PACKAGE__->mk_accessors( @PROPS, @INTERNAL );
20              
21             ####################
22             # INPUT FIELDS
23              
24             sub add_fields {
25 71     71 1 161 my ( $self, @fields ) = @_;
26 71         110 push @{ $self->{_fields} }, @fields;
  71         298  
27             }
28              
29             sub required_fields {
30 43     43 1 114 my ($self) = @_;
31 43         244 return grep { $_->requirement() eq 'required' } @{ $self->{_fields} };
  71         600  
  43         224  
32             }
33              
34             sub optional_fields {
35 0     0 1 0 my ($self) = @_;
36 0         0 return grep { $_->requirement() eq 'optional' } @{ $self->{_fields} };
  0         0  
  0         0  
37             }
38              
39             sub fields {
40 1     1 1 4 my ($self) = @_;
41 1         2 return @{ $self->{_fields} };
  1         18  
42             }
43              
44             ####################
45             # VALIDATION
46              
47             sub add_validators {
48 43     43 1 136 my ( $self, @validator_info ) = @_;
49 43         114 my @validators = ();
50 43         110 foreach my $conf (@validator_info) {
51 13         49 my $validator = $self->_factory()->get_validator( $conf->{name} );
52 13         54 my @args = $self->normalize_array( $conf->{arg} );
53 13         63 push @validators,
54             {
55             validator => $validator,
56             args => \@args
57             };
58             }
59 43         88 push @{ $self->{_validators} }, @validators;
  43         385  
60             }
61              
62             sub get_validators {
63 36     36 1 98 my ($self) = @_;
64 36 50       162 return () if ( not defined $self->{_validators} );
65 36         65 return @{ $self->{_validators} };
  36         144  
66             }
67              
68             sub validate {
69 36     36 1 129 my ( $self, $wf, $action_args ) = @_;
70 36         181 my @validators = $self->get_validators;
71 36 50       166 return unless ( scalar @validators );
72              
73 36   50     261 $action_args //= {};
74             my %all_args = (
75 36         163 %{ $wf->context->param() },
76 36         67 %{$action_args}
  36         154  
77             );
78 36         258 foreach my $validator_info (@validators) {
79 45         116 my $validator = $validator_info->{validator};
80 45         126 my $args = $validator_info->{args};
81              
82 45         113 my @runtime_args = ();
83 45         79 foreach my $arg ( @{$args} ) {
  45         159  
84 45 100       146 if ( $arg =~ /^\$(.*)$/ ) {
85 9         44 push @runtime_args, $all_args{$1};
86             } else {
87 36         111 push @runtime_args, $arg;
88             }
89             }
90 45         289 $validator->validate($wf, @runtime_args);
91             }
92             }
93              
94             # Subclasses override...
95              
96             sub execute {
97 1     1 1 428 my ( $self, $wf ) = @_;
98 1         18 croak "Class ", ref($self), " must implement 'execute()'\n";
99             }
100              
101             ########################################
102             # PRIVATE
103              
104             sub init {
105 44     44 1 150 my ( $self, $wf, $params ) = @_;
106              
107             # So we don't destroy the original...
108 44         262 my %copy_params = %{$params};
  44         317  
109              
110 43         214 $self->_factory( $wf->_factory() );
111 43         1753 $self->class( $copy_params{class} );
112 43         762 $self->name( $copy_params{name} );
113 43         910 $self->description( $copy_params{description} );
114 43         772 $self->group( $copy_params{group} );
115              
116             ## init normal fields
117 43         766 my @fields = $self->normalize_array( $copy_params{field} );
118 43         461 foreach my $field_info (@fields) {
119 71 50       194 if ( my $field_class = $field_info->{class} ) {
120 0         0 $self->log->debug("Using custom field class $field_class");
121 0         0 $self->add_fields( $field_class->new($field_info) );
122             } else {
123 71         247 $self->log->debug("Using standard field class");
124 71         1879 $self->add_fields(
125             Workflow::InputField->new($field_info) );
126             }
127             }
128              
129             ## establish validator for fields with is_required="yes"
130 43         305 @fields = $self->required_fields();
131 43         960 my $validator = Workflow::Validator::HasRequiredField->new(
132             { name => 'HasRequiredField for is_required fields',
133             class => 'Workflow::Validator::HasRequiredField'
134             }
135             );
136 43         187 my @args = ();
137 43         143 foreach my $field (@fields) {
138 58 50       505 next if ( not $field ); ## empty @fields array
139 58         148 push @args, $field->name();
140             }
141 43         425 push @{ $self->{_validators} },
  43         339  
142             {
143             validator => $validator,
144             args => \@args
145             };
146              
147             ## init normal validators
148 43         262 my @validator_info = $self->normalize_array( $copy_params{validator} );
149 43         303 $self->add_validators(@validator_info);
150              
151 43         255 delete @copy_params{(@PROPS, qw( field validator ))};
152              
153             # everything else is just a passthru param
154              
155 43         326 while ( my ( $key, $value ) = each %copy_params ) {
156 3         17 $self->param( $key, $value );
157             }
158             }
159              
160             1;
161              
162             __END__
163              
164             =pod
165              
166             =head1 NAME
167              
168             Workflow::Action - Base class for Workflow actions
169              
170             =head1 VERSION
171              
172             This documentation describes version 2.09 of this package
173              
174             =head1 SYNOPSIS
175              
176             # Configure the action... (in e.g., 'actions.yaml')
177             action:
178             - name: CreateUser
179             class: MyApp::Action::CreateUser
180             field:
181             - name: username
182             is_required: yes
183             - name: email
184             is_required: yes
185             validator:
186             - name: IsUniqueUser
187             arg:
188             - value: '$username'
189             - name: IsValidEmail
190             arg:
191             - value: '$email'
192              
193             # Define the action
194              
195             package MyApp::Action::CreateUser;
196              
197             use parent qw( Workflow::Action );
198             use Workflow::Exception qw( workflow_error );
199             use Syntax::Keyword::Try;
200              
201             sub execute {
202             my ( $self, $wf ) = @_;
203             my $context = $wf->context;
204              
205             # Since 'username' and 'email' have already been validated we
206             # don't need to check them for uniqueness, well-formedness, etc.
207              
208             my $user;
209             try {
210             $user = User->create({ username => $context->param( 'username' ),
211             email => $context->param( 'email' ) })
212             }
213             catch ($error) {
214             # Wrap all errors returned...
215             workflow_error
216             "Cannot create new user with name '", $context->param( 'username' ), "': $error";
217             }
218              
219             # Set the created user in the context for the application and/or
220             # other actions (observers) to use
221              
222             $context->param( user => $user );
223              
224             # return the username since it might be used elsewhere...
225             return $user->username;
226             }
227              
228             =head1 DESCRIPTION
229              
230             This is the base class for all Workflow Actions. You do not have to
231             use it as such but it is strongly recommended.
232              
233             =head1 CONFIGURATION
234              
235             You configure your actions and map them to a specific module in your actions
236             configuration file using the syntax
237             above and that shown in L<Workflow>. In some cases, you'll have actions
238             that apply to all workflows. In more elaborate configurations, you may have
239             one workflow server loading multiple workflows and multiple actions for each.
240             In these
241             cases, you'll have multiple workflow types and you may want actions
242             with the same names to have different behaviors for each type.
243              
244             For example, you may have a workflow type Ticket and another type Order_Parts.
245             They both may have a Submit action, but you'll want the Submit to be different
246             for each.
247              
248             You can specify a type in your actions configuration to associate that action
249             with that workflow type. If you don't provide a type, the action is available
250             to all types. For example:
251              
252             type: Ticket <-- here!
253             description: |-
254             Actions for the Ticket workflow only.
255             action:
256             - name: TIX_NEW
257             group: some_action_group # optional
258             class: TestApp::Action::TicketCreate
259             description: |- # optional
260             My action description
261              
262             The type must match an existing workflow type or the action will never
263             be called.
264              
265             =head1 STANDARD ATTRIBUTES
266              
267             Each action supports the following attributes:
268              
269             =over
270              
271             =item * C<class> (required)
272              
273             The Perl class which provides the behaviour of the action.
274              
275             =item * C<description> (optional)
276              
277             A free text field describing the action.
278              
279             =item * C<group> (optional)
280              
281             The group for use with the L<Workflow::State/get_available_action_names>
282             C<$group> filter.
283              
284             =item * C<name> (required)
285              
286             The name by which workflows can reference the action.
287              
288             =item * C<type> (optional)
289              
290             Associates the action with workflows of the same type, when set. When
291             not set, the action is available to all workflows.
292              
293             =back
294              
295              
296             These attributes (except for the C<class> attribute) all map to instance
297             properties by the same name.
298              
299              
300             =head1 ADDITIONAL ATTRIBUTES
301              
302             You can validate additional attributes in of your action by doing two things:
303              
304             =over
305              
306             =item *
307              
308             Set C<$Workflow::Factory::VALIDATE_ACTION_CONFIG> to 1.
309              
310             =item *
311              
312             Provide function validate_config() in your action class.
313              
314             =back
315              
316             Then, this function will be called with all the acton attributes when
317             it is parsed. For example, if your action XML looks like this:
318              
319             <action name="BEGIN" class="My::Class" when="NOW">
320              
321             You can validate it like this:
322              
323             sub My::Class::validate_config {
324             my $config = shift;
325             unless ('NOW' eq $config->{when}) {
326             configuration_error "`$$config{when}' is not a valid value " .
327             "for `when'";
328             }
329             }
330              
331             =head1 OBJECT METHODS
332              
333             =head2 Public Methods
334              
335             =head3 new()
336              
337             Subclasses may override this method, but it's not very common. It is
338             called when you invoke a method in your Workflow object that returns
339             an Action object, for example, methods such as $wf->get_action will
340             call this method.
341              
342             B<Your action classes usually subclass directly from Workflow::Action
343             and they I<don't> need to override this method at all>. However, under
344             some circumstances, you may find the need to extend your action
345             classes.
346              
347             =head3 init()
348              
349             Suppose you want to define some extra properties to actions but you
350             also want for some of these properties to depend on a particular
351             state. For example, the action "icon" will almost allways be the same,
352             but the action "index" will depend on state, so you can display your
353             actions in a certain order according to that particular state. Here is
354             an example on how you easily do this by overriding new():
355              
356             1) Set the less changing properties in your action definition:
357              
358             type: foo
359             action:
360             - name: Browse
361             type: menu_button
362             icon: list_icon
363             class: actual::action::class
364              
365             2) Set the state dependant properties in the state definition:
366              
367             state:
368             - name: INITIAL
369             description: |-
370             Manage Manufacturers
371             action:
372             - name: Browse
373             index: '0'
374             resulting_state: BROWSE
375             condition:
376             - name: roleis_oem_mgmt
377             - name: Create
378             index: '1'
379             resulting_state: CREATE
380             condition:
381             - name: roleis_oem_mgmt
382             - name: Back
383             index: '2'
384             resulting_state: CLOSED
385              
386             3) Craft a custom action base class
387              
388             package your::action::base::class;
389              
390             use warnings;
391             use strict;
392              
393             use parent qw( Workflow::Action );
394             use Workflow::Exception qw( workflow_error );
395              
396             # extra action class properties
397             my @EXTRA_PROPS = qw( index icon type data );
398             __PACKAGE__->mk_accessors(@EXTRA_PROPS);
399              
400             sub init {
401             my ($self, $wf, $params) = @_;
402             $self->SUPER::init($wf, $params);
403             # set only our extra properties from action class def
404             foreach my $prop (@EXTRA_PROPS) {
405             next if ( $self->$prop );
406             $self->$prop( $params->{$prop} );
407             }
408             # override specific extra action properties according to state
409             my $wf_state = $wf->_get_workflow_state;
410             my $action = $wf_state->{_actions}->{$self->name};
411             $self->index($action->{index});
412             }
413              
414              
415             1;
416              
417             4) Use your custom action base class instead of the default
418              
419             package actual::action::class;
420              
421             use warnings;
422             use strict;
423              
424             use parent qw( your::base::action::class );
425             use Workflow::Exception qw( workflow_error );
426              
427             sub execute {
428             ...
429             }
430              
431             1;
432              
433              
434             =head3 required_fields()
435              
436             Return a list of L<Workflow::InputField> objects that are required.
437              
438             =head3 optional_fields()
439              
440             Return a list of L<Workflow::InputField> objects that are optional.
441              
442             =head3 fields()
443              
444             Return a list of all L<Workflow::InputField> objects
445             associated with this action.
446              
447              
448             =head2 Private Methods
449              
450             =head3 init( $workflow, \%params )
451              
452             init is called in conjuction with the overall workflow initialization.
453              
454             It sets up the necessary validators based on the on configured actions, input fields and required fields.
455              
456             =head3 add_field( @fields )
457              
458             Add one or more L<Workflow::InputField>s to the action.
459              
460             =head3 add_validators( @validator_config )
461              
462             Given the 'validator' configuration declarations in the action
463             configuration, ask the L<Workflow::Factory> for the
464             L<Workflow::Validator> object associated with each name and store that
465             along with the arguments to be used, runtime and otherwise.
466              
467             =head3 get_validators()
468              
469             Get a list of all the validator hashrefs, each with two keys:
470             'validator' and 'args'. The 'validator' key contains the appropriate
471             L<Workflow::Validator> object, while 'args' contains an arrayref of
472             arguments to pass to the validator, some of which may need to be
473             evaluated at runtime.
474              
475             =head3 validate( $workflow, $action_args )
476              
477             Run through all validators for this action, using the arguments
478             provided with the C<execute_action> call. If any fail they will
479             throw a L<Workflow::Exception>, the validation subclass.
480              
481             =head3 execute( $workflow )
482              
483             Subclasses B<must> implement -- this will perform the actual
484             work. Must B<not> return a non-scalar reference or blessed object but
485             can return undef. If the action may be used in a L<Workflow::State> object
486             that has multiple resulting states you should return a simple scalar.
487              
488              
489             =head3 add_fields
490              
491             Method to add fields to the workflow. The method takes an array of
492             fields.
493              
494             =head1 SEE ALSO
495              
496             =over
497              
498             =item * L<Workflow>
499              
500             =item * L<Workflow::Factory>
501              
502             =back
503              
504             =head1 COPYRIGHT
505              
506             Copyright (c) 2003-2021 Chris Winters. All rights reserved.
507              
508             This library is free software; you can redistribute it and/or modify
509             it under the same terms as Perl itself.
510              
511             Please see the F<LICENSE>
512              
513             =head1 AUTHORS
514              
515             Please see L<Workflow>
516              
517             =cut