File Coverage

blib/lib/Decision/ParseTree.pm
Criterion Covered Total %
statement 15 36 41.6
branch 0 10 0.0
condition 0 3 0.0
subroutine 5 6 83.3
pod 1 1 100.0
total 21 56 37.5


line stmt bran cond sub pod time code
1             package Decision::ParseTree;
2              
3 1     1   34076 use base qw{Exporter};
  1         3  
  1         107  
4             our @EXPORT_OK = qw{ParseTree};
5              
6 1     1   6 use warnings;
  1         2  
  1         30  
7 1     1   6 use strict;
  1         18  
  1         100  
8              
9             =head1 NAME
10              
11             Decision::ParseTree - Replacing waterfall IF-ELSIF-ELSE blocks
12              
13             =head1 VERSION
14              
15             Version 0.041
16              
17             =cut
18              
19             our $VERSION = '0.041';
20              
21             =head1 SYNOPSIS
22              
23             Death to long if-elsif-else blocks that are hard to maintain, and hard to
24             explain to your manager. Heres an overly simplistic example:
25              
26             =head2 OLD CODE
27              
28             if ( $obj->is_numeric ) {
29             if ( $obj->is_positive ) {
30             print 'Positive Number';
31             }
32             elsif ( $obj->is_negative )
33             print 'Negative Number';
34             }
35             else {
36             print 'Looks like zero';
37             }
38             else {
39             print 'Non-Numeric Value';
40             }
41              
42             =head2 NEW CODE
43              
44             =head3 YAML Decision Tree
45              
46             ---
47             - is_num :
48             0 : Non-Numeric Value
49             1 : - is_pos :
50             1 : Positive Number
51             - is_neg :
52             = : Looks like zero
53             1 : Negative Number
54             ...
55              
56              
57             =head3 Rules Object
58              
59             package Rules;
60             use Scalar::Util;
61            
62             sub is_num {
63             my ( $self, $obj ) = @_;
64             return (Scalar::Util::looks_like_number($obj->{value})) ? 1 : 0;
65             }
66              
67             sub is_pos {
68             my ( $self, $obj ) = @_;
69             return ($obj->{value} > 0 ) ? 1 : 0;
70             }
71              
72             sub is_neg {
73             my ( $self, $obj ) = @_;
74             return ($obj->{value} < 0 ) ? 1 : 0;
75             }
76              
77             =head3 Goal Object to be passed thru the rules
78              
79             package Number;
80            
81             sub new {
82             my ( $class, $value ) = @_
83             my $self = { parse_path => [],
84             value => $value };
85             return bless $self, $class;
86             }
87            
88             =head3 Replacement to that if-else block
89              
90             use Decision::ParseTree q{ParseTree};
91              
92             my $rules = Rules->new;
93             my $tree = LoadFile('tree.yaml');
94            
95             print ParseTree( $tree, $rules, Number->new(10) ); # Positive Number
96             print ParseTree( $tree, $rules, Number->new(-1) ); # Negative Number
97             print ParseTree( $tree, $rules, Number->new(0) ); # Looks like zero
98             print ParseTree( $tree, $rules, Number->new('a')); # Non-Numeric Value
99              
100              
101              
102              
103             =head1 DESCRIPTION
104              
105             =head1 YAML as a Decision tree
106              
107             To make this all work we need a few parts:
108              
109             =over
110              
111             =item * A rules object: This will be a library of rules.
112              
113             =item * An object that will be passed thru the rules.
114              
115             =item * A YAML doc that outlines your decision tree.
116              
117             =back
118              
119             =head2 Why YAML
120              
121             So this all started as a way to make a decision tree thats easy to parse and
122             easy to read for non-programmers. So to do this I looked to YAML, it's easy
123             to read and easy to parse. Though make this work we have some hard and fast
124             rules to follow for the tree construction:
125              
126             =over
127              
128             =item * RULES are a key value pair
129              
130             =over
131              
132             =item * the key is the method to run in the rules object
133              
134             =item * the value must be an arrayref or hashref
135              
136             =back
137              
138             =item * ARRAYS are a series of rules run in order
139              
140             =item * HASHES are a series of answers
141              
142             =item * SCALARS are endpoints
143              
144             =back
145              
146             =head2 Why add more parts, why blow everything in to separate objects.
147              
148             Sometimes you have to make things messy before they can get clean.
149              
150             Theres a flexibility that comes with breaking things apart in to nice, neat
151             little chunks. By separating the rule logic in to one place you can make
152             very complex rules that do not gunk up your code. You pull the order of these
153             rules in to another place as it's completely possible that you would want to
154             tweak the order. And lastly you need to glue these separate things together,
155             so you have an object that gets passed thru to make this all work. Tada!
156              
157             =head2 Examples
158              
159             It would be nice to whip up a big example here to show all the interesting
160             bits, sadly I can't think of a good example. Ideas?
161              
162             =over
163              
164             =item * Selecting a tests to run for hardware
165              
166             =item * Building settings/configuration files on the fly for varried hardware.
167              
168             =item * Would any one like to use this to write up a GO AI engine? Chess?
169              
170             =back
171              
172             =head1 FEATURES
173              
174             =over
175              
176             =item * tracking for free
177              
178             =over
179              
180             =item * If $obj->{parse_path} exists then every step that this obj takes thru
181             the rules will be tracked. This path will be stored as an array ref, of hash refs.
182              
183             $obj = Number->new(10);
184             ParseTree( $tree, $rules, $obj );
185             # $obj->{parse_path} will now look like :
186             # [ { 'is_num' => 1 },
187             # { 'is_pos' => 1 },
188             # ]
189              
190             =item * If $obj->{parse_answer} exists then, when an answer is found, then it
191             gets stored here as well as being returned.
192              
193             print $obj->{parse_answer}; # Positive Number
194              
195             =back
196              
197             =back
198              
199             =head1 EXPORT OK-able
200              
201             ParseTree is the only thing that can get exported, it's also the only thing in
202             here, so export away.
203              
204             =head1 FUNCTIONS
205              
206             =head2 ParseTree($tree, $rules, $obj)
207              
208             Runs $obj thru $tree, using $rules as the library of rules.
209              
210             Returns the first endpoint that you run into as the answer.
211              
212             =cut
213              
214             #=== FUNCTION ================================================================
215             # NAME: ParseTree
216             # PURPOSE: walk a decision tree to get an answer
217             # PARAMETERS: $tree : Expected to be a big array ref of stuff pulled from YAML
218             # $rules: an object of rules that holds $tree's nodes
219             # $obj : The concept is that this $obj is what is passed thru the
220             # rules. So build your rules as though $obj will be passed
221             # to them.
222             # Also, there are two 'plugins' for $obj:
223             # $obj->{parse_path} : if exists it will contain the path
224             # that the $obj took
225             # $obj->{parse_answer} : if exists it will hold the result
226             # RETURNS: the proper value from $tree or undef
227             # THROWS: there are many assertions that will die on failure
228             # COMMENTS: none
229             # SEE ALSO: the pod above for an explination and example
230             #===============================================================================
231              
232             sub ParseTree {
233 1     1   791 use YAML; # to get YAML::Value
  1         10157  
  1         59  
234 1     1   934 use Carp::Assert::More;
  1         4039  
  1         486  
235 0     0 1   my($tree, $rules, $obj) = @_;
236              
237 0           assert_listref( $tree, q{A list of rules must be an array.} );
238              
239 0           NODE : foreach my $task (@$tree) {
240 0           assert_hashref( $task, q{Task nodes must be a hashref.} );
241            
242             #---------------------------------------------------------------------------
243             # grab the values as they are the answers that we will check agenst
244             #---------------------------------------------------------------------------
245 0           my ($answers) = values(%$task);
246 0           assert_hashref( $answers, q{You answers need to be presented as a hashref.} );
247              
248             #---------------------------------------------------------------------------
249             # grab the action
250             #---------------------------------------------------------------------------
251 0           my ($action) = keys %$task;
252              
253             #---------------------------------------------------------------------------
254             # run the action to get the reply
255             #---------------------------------------------------------------------------
256 0           assert_defined( $rules->can($action), q{Your rule needs to exist in your rules object.} );
257 0           my $reply = $rules->$action($obj);
258              
259             #---------------------------------------------------------------------------
260             # Log to the obj if theres a place to log to
261             #---------------------------------------------------------------------------
262 0 0         if (defined $obj->{parse_path}) {
263 0           push @{$obj->{parse_path}}, {$action => $reply};
  0            
264             }
265              
266             #---------------------------------------------------------------------------
267             # handle default YAML values if they exist if not by spec if we get
268             # undef back we continue to the next node
269             #---------------------------------------------------------------------------
270 0 0 0       if( !defined( $reply )
271             || !defined( $answers->{$reply} )
272             ) {
273 0 0         if( defined $answers->{YAML::VALUE} ) {
274             # YAML::Value is a constant in YAML that specifies any default (=) key
275 0           $reply = YAML::VALUE;
276             } else {
277 0           next NODE; #continue if $reply is not an $answer
278             }
279             }
280              
281             #---------------------------------------------------------------------------
282             # Deal with sub trees
283             #---------------------------------------------------------------------------
284 0 0         return ParseTree($answers->{$reply}, $rules, $obj)
285             if ref($answers->{$reply}) eq q{ARRAY};
286            
287             #---------------------------------------------------------------------------
288             # Deal with our answer
289             #---------------------------------------------------------------------------
290 0 0         if (defined $obj->{parse_answer}) {
291 0           $obj->{parse_answer} = $answers->{$reply};
292             }
293 0           return $answers->{$reply};
294            
295             }
296 0           return undef; #catch all failure... this should never happen
297             }
298             =head1 CAVEATS / TODO
299              
300             =over
301              
302             =item * Currently $tree is expected to be a pre-parsed YAML File, This should
303             change here soon to also accept a filename. Currently though it does not.
304              
305             =item * would like even more examples.
306              
307             =item * need to flush out the docs more.
308              
309             =back
310              
311             =head1 AUTHOR
312              
313             ben hengst, C<< >>
314              
315             =head1 BUGS
316              
317             Please report any bugs or feature requests to
318             C, or through the web interface at
319             L.
320             I will be notified, and then you'll automatically be notified of progress on
321             your bug as I make changes.
322              
323             =head1 SUPPORT
324              
325             You can find documentation for this module with the perldoc command.
326              
327             perldoc Decision::ParseTree
328              
329             You can also look for information at:
330              
331             =over 4
332              
333             =item * AnnoCPAN: Annotated CPAN documentation
334              
335             L
336              
337             =item * CPAN Ratings
338              
339             L
340              
341             =item * RT: CPAN's request tracker
342              
343             L
344              
345             =item * Search CPAN
346              
347             L
348              
349             =back
350              
351             =head1 ACKNOWLEDGEMENTS
352              
353             =head1 COPYRIGHT & LICENSE
354              
355             Copyright 2007 ben hengst, all rights reserved.
356              
357             This program is free software; you can redistribute it and/or modify it
358             under the same terms as Perl itself.
359              
360             =cut
361              
362             1; # End of Decision::ParseTree