File Coverage

lib/Function/Runner.pm
Criterion Covered Total %
statement 104 116 89.6
branch 26 42 61.9
condition 1 3 33.3
subroutine 14 16 87.5
pod 0 6 0.0
total 145 183 79.2


line stmt bran cond sub pod time code
1             package Function::Runner;
2              
3 2     2   132006 use strict; use warnings; use utf8; use 5.10.0;
  2     2   18  
  2     2   45  
  2     2   10  
  2         2  
  2         48  
  2         8  
  2         2  
  2         10  
  2         66  
  2         9  
4 2     2   9 use Data::Dumper;
  2         8  
  2         2934  
5             our $VERSION = '0.003';
6              
7              
8             my $PEEK_LEVEL = 5; # Disallow peeks below this level
9             sub peek { # ( $level, $res ) --> $res
10 95     95 0 810 my ($level, $res) = @_;
11              
12             # Comment out logging in PROD
13 95         116 _log_save($res);
14              
15             # Guard: Do nothing if $level is lower than PEEK_LEVEL
16 95 50       141 return $res if $level < $PEEK_LEVEL;
17              
18             # Print content of $res
19 0         0 my $file = (caller(0))[1];
20 0         0 my $line = (caller(0))[2];
21              
22 0         0 say "$file line $line: ". Dumper $res;
23 0         0 return $res;
24             }
25              
26             my $LOG = []; # Container for logs from peek()
27             sub _log_save { # ($res)
28 95     95   107 my $res = shift;
29             # Add to log regardless of PEEK_LEVEL
30 95         331 my $pkg = (caller(1))[0];
31 95         245 my $file = (caller(1))[1];
32 95         228 my $line = (caller(1))[2];
33 95         250 push @$LOG, ["file:$file - pkg:$pkg - line:$line: ",$res];
34             #push @$LOG, "file:$file - pkg:$pkg - line:$line: ".$res;
35 95         104 return $res;
36             }
37 2     2   13 sub _log_fetch { return $LOG }
38 1     1   542 sub _log_clear { $LOG = [] }
39              
40              
41             ## CONSTRUCTORS
42             sub new {
43             # Clear the LOG
44 2     2 0 1270 $LOG = [];
45              
46 2         3 my $fn_map = {}; # initial function map
47 2         3 my $defn = $_[1]; # user-provided function definition
48 2         4 my $pkg = (caller)[0]; # calling package
49 2 50 33     12 _die("missing defn or pkg") unless defined $defn && defined $pkg;
50              
51             # See: https://perldoc.perl.org/perlmod#Symbol-Tables
52 2         92 my $tab = eval '\%'.$pkg.'::'; # symbol table of calling package
53 2         14 peek 3, ['Symbol Table: ','\%'.$pkg.'::',"has ref: \"".ref($tab).'"'];
54              
55 2         8 _mk_fn_map($fn_map,$defn,$tab,$pkg);# build fn_map from $defn and $tab
56 2         5 peek 3, ['Completed fn_map: ',$fn_map];
57              
58 2         18 bless { defn=>$defn,
59             fn=>$fn_map,
60             log=>{ step => [], # Store steps and results
61             func => [] } # Store funcs and results
62             },
63             $_[0];
64             }
65              
66              
67             ## METHODS
68             my $LEVEL = 0; # Tracks recursion levels
69             sub _mk_fn_map {
70 4     4   8 my ($fn_map, $defn, $tab, $pkg) = @_;
71              
72             # Walk the defn, get all coderefs
73 4         10 foreach my $step (keys %$defn) {
74 9         10 my $res = $defn->{$step};
75 9         11 my $ref = ref $res;
76 9         27 peek 3, ["Processing StepDef: $step",$res, " has res: \"$ref\""];
77              
78 9 100       15 if ($ref eq '') { # Coderef. e.g. '&bye' or '/greet'
    50          
79             # Guard: Skip if Step not Func
80             # Step Example: '/greet'
81             # Func Example: '&bye'
82 7 100       16 if ($res =~ /^\/(.*)/) {
83 2         5 peek 3, "Ignored StepDef when building fn_map: $res";
84 2         3 next;
85             }
86              
87 5         15 my ($sym) = ($res =~ /^&(.*)/);
88 5         11 peek 3, "Processing Func: $res";
89 5 50       9 _die("Bad res: $res") unless defined $sym;
90              
91             # Guard: Skip if already in $fn_map
92 5 100       9 if (exists $fn_map->{$sym}) {
93 1         3 peek 3, "Func already mapped: $res";
94 1         2 next;
95             }
96              
97             # Guard: The given symbol e.g. 'hello' must be defined as a
98             # function in the calling package
99 4 50       173 my $is_code = eval 'defined &'.$pkg.'::'.$sym ? 1 : 0;
100 4         19 peek 3, "Func: $res is code: \"$is_code\"";
101 4 50       9 _die("\n\n"."\"$sym\" not a coderef in \"$pkg\"")
102             unless $is_code;
103              
104             # Add mapping of symbol to coderef
105 4         12 $fn_map->{$sym} = $tab->{$sym};
106 4         8 peek 3, "Add to fn_map: $res";
107              
108             } elsif ($ref eq 'HASH') { # Defn e.g. { ':ok' => ... }
109 2         2 $LEVEL++;
110 2         7 peek 3, ["Descending into: $step to ---- Level ".$LEVEL.' ----'];
111 2         5 _mk_fn_map($fn_map, $res, $tab, $pkg);
112 2         4 $LEVEL--;
113 2         6 peek 3, ["Ascending from: $step to ---- Level ".$LEVEL.' ----'];
114              
115             } else {
116 0         0 _die("Unexpected ref type: $ref");
117              
118             }
119             }
120             }
121             sub call {
122 12     12 0 19 my ($o,$func,@args) = @_;
123              
124             #Guard: Func must exist
125 12 50       20 _die("Func does not exist: $func") unless exists $o->{fn}{$func};
126              
127             #peek 3, "call $func() with args: ". join ', ',@args;
128              
129 12         22 my ($fn_res,@new_args) = $o->{fn}{$func}->(@args);
130              
131             # Log the func that was called
132 12         275 push @{$o->{log}{func}}, {"&$func" => ":$fn_res"};
  12         43  
133              
134 12         42 return ($fn_res,@new_args);
135             }
136             sub run { # ($step) -> $result
137 10     10 0 520 my ($o,$step,@args) = @_;
138 10         8 my @run_result;
139              
140             #Guard: Step must exist
141 10 50       21 _die("Step does not exist: $step") unless exists $o->{defn}{$step};
142              
143             # Clear the logs if at LEVEL 0
144 10 100       17 if ($LEVEL == 0) {
145 6         20 $o->{log} = { step=>[], func => [] };
146             }
147              
148 10         26 peek 3, "------- $LEVEL -------";
149              
150              
151 10         16 my $def = $o->{defn}{$step};
152 10         11 my $ref = ref $def;
153 10 100       22 if ($ref eq '') { # e.g. '&bye'
    50          
154             # Get the function to run
155 4         32 my ($fn) = ($def =~ /^&(.*)/);
156 4 50       8 _die("Defn is not a function: $def") unless defined ($fn);
157 4         14 peek 3, "Step $step calls function $fn()";
158              
159             # Call the function, return the result
160 4         9 peek 3, "Call $fn() and return the result";
161 4         11 @run_result = $o->call($fn,@args);
162              
163             } elsif ($ref eq 'HASH') { # e.g. { 'run' => '&checkSwitch', ... }
164 6         6 $LEVEL++;
165 6         14 peek 1, ["Descending non-terminal step: ".$LEVEL, $def];
166              
167             # Guard: The 'run' attribute must exist in the definition
168 6 50       12 _die("Defn of $step missing 'run' attribute") unless defined $def->{run};
169              
170             # Get the function to run
171 6         22 my ($fn) = ($def->{run} =~ /^&(.*)/);
172 6 50       9 _die("Defn of $step is not a function: $def") unless defined ($fn);
173 6         16 peek 3, "Step $step calls function $fn()";
174              
175             # Call the function, save the result
176 6         13 peek 3, "Call $fn() and save the result";
177 6         11 my ($fn_res,@new_args) = $o->call($fn,@args);
178 6         16 peek 3, " Result of calling $fn() is $fn_res";
179              
180             # Log step that was ran
181 6         5 push @{$o->{log}{step}}, [$step, "&$fn",":$fn_res"];
  6         15  
182              
183             # Get the next step pointed to by the result
184 6         10 my $next_step = $def->{':'.$fn_res};
185              
186             # Guard: The next step must exist for non-terminal (HASHREF) steps
187 6 50       9 _die("Next step of $step:$fn_res undefined") unless defined $next_step;
188              
189 6         13 peek 1, ["The next step and args:", $next_step, [@new_args]];
190              
191              
192             # If next step is a Func, call it
193             # If next step is a Step, run it
194             # Else error
195 6 100       24 if ($next_step =~ /^&.+/) {
    50          
196 2         15 my ($next_func) = $next_step =~ /^&(.+)/;
197 2         5 peek 3, " Next step is a Func, calling it..";
198 2         4 @run_result = $o->call($next_func,@new_args);
199             } elsif ($next_step =~ /^\/.+/) {
200 4         10 peek 3, " Next step is a Step, running it..";
201 4         30 @run_result = $o->run($next_step,@new_args);
202             }
203              
204 6         7 $LEVEL--;
205 6         15 peek 1, ["Ended non-terminal step: ".$LEVEL, $def];
206              
207 6         14 return @run_result;
208             } else {
209 0         0 _die("Unexpected Step type: $ref");
210             }
211             }
212 1     1 0 6 sub steps { return shift->{log}{step} }
213 0     0 0   sub funcs { return shift->{log}{func} }
214              
215              
216             # PRIVATE HELPERS
217             sub _die {
218             # Private method to display all errors and then die
219 0     0     my ($o,$msg) = @_;
220              
221 0           my ($pkg,$file,$line) = caller;
222              
223             # Case called as a function, message is the first arg
224 0 0         if (ref $o eq '') { $msg = $o }
  0            
225              
226 0           die Dumper($LOG)
227             ."\n $msg\n (pkg: $pkg - file: $file - line: $line)\n ";
228             }
229              
230              
231             1;
232              
233             =encoding utf-8
234             =cut
235             =head1 NAME
236              
237             Function::Runner - Define functions at a higher level and run them
238              
239             =cut
240             =head1 SYNOPSIS
241              
242             use Function::Runner;
243              
244             # Hello World
245             sub greet {
246             print "Hello ". ($_[0] || 'World') ."\n";
247             return ('ok',$_[0]);
248             }
249              
250             my $defn = { # Definition is just a hashref
251             '/hello' => '&greet' # The /hello step,
252             }; # calls the &greet function
253              
254             my $fn = Function::Runner->new($defn); # Create a greeter
255             $fn->run('/hello','Flash'); # Hello Flash
256              
257              
258             my $switch = { # Define a switch
259             '/checkSwitch' => {
260             'run' => '&checkSwitch', # Check the switch
261             ':on' => '&bye', # If it's on, leave
262             ':off' => '/turnOn', # If it's off, turn it on
263             },
264             '/turnOn' => { # Turn on the switch
265             'run' => '&greet', # Greet the caller
266             ':ok' => '/turnOff', # Then turn off the switch
267             },
268             '/turnOff' => '&bye', # Turn off the switch and leave
269             };
270             sub bye {
271             print "Bye ". ($_[0] || 'World') ."\n";
272             return ('ok',$_[0]);
273             }
274             sub checkSwitch { return @_ }
275              
276             $fn = Function::Runner->new($switch); # Create a switch
277             $fn->run('/checkSwitch', 'on', 'Flash'); # Bye Flash
278              
279             $fn->run('/checkSwitch', 'off', 'Hulk'); # Hello Hulk
280             # Bye Hulk
281              
282             say join ' ', @$_ for @{$fn->steps}; # List steps, function and result
283             # /checkSwitch &checkSwitch :off
284             # /turnOn &greet :ok
285              
286             =cut
287             =head1 DESCRIPTION
288              
289             Function::Runner provides a way to define the steps of a function and
290             the logical flow between the steps using just hashrefs. The user then
291             implements the steps that need to be called. The function runner will
292             then run the function.
293              
294             This module is handy for functions that are naturally composed of many
295             hierarchical steps and flows differently depending on the results of
296             those steps. The function definition helps to clarify the steps and flow
297             at a higher level.
298              
299             A function definition (B) is composed of three (3) constructs:
300             I, I and I. Each construct is a string with a
301             different character prefix to indicate the kind of construct:
302              
303             /a_step # Steps are prefixed with /, like directories
304              
305             &a_function # Functions prefixed with &, like Perl
306              
307             :some_result # Results prefixed with :
308              
309             The keys of the funcdef hashref is always a I. The value of the
310             funcdef hashref is the step definition (B) defines how that step
311             is to be executed.
312              
313             A I can be just a I if no further steps follow. For
314             example:
315              
316             { '/hello' => '&greet' }
317              
318             A I can also be a hashref that defines the I to run
319             and the I to take depending on the I of that
320             function run. For example:
321              
322             '/checkSwitch' => {
323             'run' => '&checkSwitch', # Check the switch
324             ':on' => '&bye', # If it's on, leave
325             ':off' => '/turnOn', # If it's off, turn it on
326             },
327              
328             The next step can either be a function:
329              
330             ':on' => '&bye' # On "on" result, call the &bye function
331              
332             or it can be another step:
333              
334             ':off' => '/turnOn' # On "off" result, run the /turnOn step
335              
336             =cut
337             =head1 METHODS
338             =cut
339             =head2 run($step,@args)
340              
341             The run() method runs the given $step, checks the results, looks up the
342             function definition to determine the next step to run an calls that
343             until there is nothing left to be done at which point it will return the
344             result of the last function that was called.
345              
346             Along the way it tracks how deep it is within the function definition.
347             Each step that was ran and the corresponding result is stored in an
348             array.
349              
350             =cut
351             =head2 steps()
352              
353             The steps() method returns the steps that ran for that function.
354              
355             =cut
356             =head1 NAMING CONVENTIONS
357              
358             I and I with the form B is rather
359             pleasing to read. For example:
360              
361             &findStalledWorkers
362              
363             /find_stalled_workers
364              
365             I with the form B or B is
366             also rather pleasing to read. For example:
367              
368             :queueEmpty
369              
370             :not_found
371              
372             Taken together, the step and it's results end up reading like this:
373              
374             /find_stalled_workers :not_found
375              
376             =cut
377             =head1 NOTES
378              
379             Defining a function in terms of I, I and I
380             has several nice properties.
381              
382             The valid return values from each I is clearly spelled out.
383              
384             Having a I makes it easier rearrange the flow of steps within
385             that function or add an additional step in the function's processing.
386              
387             It is possible to directly call the steps in a function definition.
388              
389             It is possible to analyze the I hashref to create a reverse
390             dependency graph so that when a I is about to be changed, find
391             all it's dependents.
392              
393             All in all, it is a rather nice way to design these kinds of
394             hierarchical, multi-step functions where the flow depends on the results
395             of prior steps.
396              
397             =cut
398             =head1 AUTHOR
399              
400             Hoe Kit CHEW Ehoekit@gmail.comE
401              
402             =cut
403             =head1 COPYRIGHT
404              
405             Copyright 2021- Hoe Kit CHEW
406              
407             =cut
408             =head1 LICENSE
409              
410             This library is free software; you can redistribute it and/or modify
411             it under the same terms as Perl itself.
412              
413             =cut
414