File Coverage

blib/lib/VS/RuleEngine/Loader/XML.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package VS::RuleEngine::Loader::XML;
2              
3 10     10   325223 use strict;
  10         25  
  10         466  
4 10     10   55 use warnings;
  10         20  
  10         365  
5              
6 10     10   56 use Carp qw(croak);
  10         34  
  10         894  
7 10     10   62 use Scalar::Util qw(blessed);
  10         17  
  10         1256  
8 10     10   19328 use XML::LibXML;
  0            
  0            
9              
10             use VS::RuleEngine::Engine;
11             use VS::RuleEngine::Util qw(is_existing_package);
12              
13             use Object::Tiny qw(_ruleset);
14              
15             our $VERSION = "0.05";
16              
17             sub _new {
18             my ($pkg) = @_;
19             my $self = bless {
20             _ruleset => {},
21             }, $pkg;
22             return $self;
23             }
24              
25             sub load_file {
26             my ($self, $path) = @_;
27            
28             my $parser = XML::LibXML->new();
29             my $doc = $parser->parse_file($path);
30             my $engine = $self->_process_document($doc);
31            
32             return $engine;
33             }
34              
35             sub load_string {
36             my ($self, $xml) = @_;
37            
38             my $parser = XML::LibXML->new();
39             my $doc = $parser->parse_string($xml);
40             my $engine = $self->_process_document($doc);
41            
42             return $engine;
43             }
44              
45             {
46             my %Node_Handler = (
47             action => "_process_action",
48             defaults => "_process_defaults",
49             input => "_process_input",
50             output => "_process_output",
51             posthook => "_process_posthook",
52             prehook => "_process_prehook",
53             rule => "_process_rule",
54             ruleset => "_process_ruleset",
55             run => "_process_run",
56             );
57            
58             sub _process_document {
59             my ($self, $doc) = @_;
60            
61             $self = __PACKAGE__->_new() unless blessed $self;
62              
63             # Clear rulesets
64             $self->{_ruleset} = {};
65            
66             my $root = $doc->documentElement();
67             croak ("Expected root node 'engine' but found '", $root->nodeName, "'") if $root->nodeName ne "engine";
68            
69             my $engine_class = "VS::RuleEngine::Engine";
70             if ($root->hasAttribute("instanceOf")) {
71             my $class = $root->getAttribute("instanceOf");
72             if (!is_existing_package($class)) {
73             eval "require ${class};";
74             croak $@ if $@;
75             }
76            
77             $engine_class = $class;
78             }
79              
80             my $engine = $engine_class->new();
81            
82             # Iterate over child nodes
83             for my $child ($root->childNodes) {
84             # Skip stuff that's not elements
85             next unless $child->isa("XML::LibXML::Element");
86            
87             my $name = $child->nodeName;
88             my $handler = $Node_Handler{$name};
89             croak "Don't know how to handle '${name}'" if !$handler;
90              
91             $self->$handler($child, $engine);
92             }
93            
94             return $engine;
95             }
96             }
97              
98             sub _process_action {
99             my ($self, $action, $engine) = @_;
100             my ($name, $class, $defaults, @args) = $self->_process_std_element($action);
101             $engine->add_action($name => $class, $defaults, @args);
102             };
103              
104             sub _process_input {
105             my ($self, $input, $engine) = @_;
106             my ($name, $class, $defaults, @args) = $self->_process_std_element($input);
107             $engine->add_input($name => $class, $defaults, @args);
108             };
109              
110             sub _process_output {
111             my ($self, $output, $engine) = @_;
112             my ($name, $class, $defaults, @args) = $self->_process_std_element($output);
113             $engine->add_output($name => $class, $defaults, @args);
114             };
115              
116             sub _process_prehook {
117             my ($self, $hook, $engine) = @_;
118             my ($name, $class, $defaults, @args) = $self->_process_std_element($hook);
119             $engine->add_hook($name => $class, $defaults, @args);
120             $engine->add_pre_hook($name);
121             };
122              
123             sub _process_posthook {
124             my ($self, $hook, $engine) = @_;
125             my ($name, $class, $defaults, @args) = $self->_process_std_element($hook);
126             $engine->add_hook($name => $class, $defaults, @args);
127             $engine->add_post_hook($name);
128             };
129              
130             sub _process_rule {
131             my ($self, $rule, $engine) = @_;
132             my ($name, $class, $defaults, @args) = $self->_process_std_element($rule);
133             $engine->add_rule($name => $class, $defaults, @args);
134             };
135              
136             sub _process_defaults {
137             my ($self, $defaults, $engine) = @_;
138            
139             my $name = $defaults->getAttribute("name");
140              
141             my @args;
142             for my $arg ($defaults->childNodes) {
143             next unless $arg->isa("XML::LibXML::Element");
144             my $name = $arg->nodeName;
145             my $value = $arg->hasChildNodes ? $arg->textContent : undef;
146             push @args, $name => $value;
147             }
148            
149             my $data = { @args };
150            
151             $engine->add_defaults($name, $data);
152             }
153              
154             sub _process_ruleset {
155             my ($self, $ruleset, $engine) = @_;
156            
157             my $name = $ruleset->getAttribute("name");
158            
159             croak "Ruleset '${name}' is already defined" if exists $self->_ruleset->{$name};
160            
161             # This does not apply to all rules in the engine
162             # but rather to the ones we've added so far when
163             # parsing
164             my @rules;
165            
166             if ($ruleset->hasAttribute("rulesMatchingName")) {
167             my $s = $ruleset->getAttribute("rulesMatchingName");
168             my $re = qr/$s/;
169            
170             my @matching_rules = sort grep { $_ =~ $re } $engine->rules;
171             push @rules, @matching_rules;
172             }
173              
174             if ($ruleset->hasAttribute("rulesOfClass")) {
175             my $c = $ruleset->getAttribute("rulesOfClass");
176             my @matching_rules = sort grep {
177             my $rule = $engine->_get_rule($_);
178             UNIVERSAL::isa($rule->_pkg, $c)
179             } $engine->rules;
180            
181             push @rules, @matching_rules;
182             }
183            
184             push @rules, $self->_process_rules($ruleset, $engine);
185            
186             @rules = sort keys %{+{ map { $_ => 1 } @rules }};
187            
188             $self->_ruleset->{$name} = \@rules;
189             }
190              
191             sub _process_run {
192             my ($self, $run, $engine) = @_;
193            
194             croak "Missing attribute 'action' for element 'run'" unless $run->hasAttribute("action");
195             my $action = $run->getAttribute("action");
196             croak "No action named '${action}' exists" unless $engine->has_action($action);
197            
198             my @rules = $self->_process_rules($run, $engine);
199            
200             for my $rule (@rules) {
201             $engine->add_rule_action($rule => $action);
202             }
203             }
204              
205             sub _process_std_element {
206             my ($self, $element) = @_;
207              
208             if (!$element->hasAttribute("name")) {
209             croak $element->nodeName, " is missing mandatory attribute 'name'";
210             }
211             my $name = $element->getAttribute("name");
212              
213             if (!$element->hasAttribute("instanceOf")) {
214             croak $element->nodeName, " is missing mandatory attribute 'instanceOf'";
215             }
216             my $class = $element->getAttribute("instanceOf");
217            
218             my $defaults = $element->getAttribute("defaults");
219             $defaults = "" if !defined $defaults;
220             $defaults = [split/,\s*|\s+/, $defaults];
221              
222             my @args = $self->_process_args($element, $class);
223              
224             return ($name, $class, $defaults, @args);
225             }
226              
227             sub _process_rules {
228             my ($self, $element, $engine) = @_;
229            
230             my @rules;
231            
232             for my $rule ($element->childNodes) {
233             next unless $rule->isa("XML::LibXML::Element");
234             my $name = $rule->textContent;
235             my $type = $rule->nodeName;
236            
237             ($name) = $name =~ /^\s*(.*?)\s*$/;
238             croak "Empty '${type}' name" if $name eq '';
239            
240             if ($type eq 'rule') {
241             croak "No rule named '${name}' exists" unless $engine->has_rule($name);
242             push @rules, $name;
243             }
244             elsif ($type eq 'ruleset') {
245             croak "No ruleset named '${name}' exists" if !exists $self->_ruleset->{$name};
246             push @rules, @{$self->_ruleset->{$name}};
247             }
248             else {
249             croak "Expected rule or ruleset element but got '${type}'";
250             }
251             }
252            
253             @rules = sort keys %{+{ map { $_ => 1 } @rules }};
254            
255             return @rules;
256             }
257              
258             sub _process_args {
259             my ($self, $element, $class) = @_;
260              
261             if (!is_existing_package($class)) {
262             eval "require ${class};";
263             croak $@ if $@;
264             }
265            
266             if ($class->can("process_xml_loader_args")) {
267             return $class->process_xml_loader_args($element);
268             }
269            
270             my @args;
271             for my $arg ($element->childNodes) {
272             next unless $arg->isa("XML::LibXML::Element");
273             my $name = $arg->nodeName;
274             my $value = $arg->hasChildNodes ? $arg->textContent : undef;
275             push @args, $name => $value;
276             }
277            
278             return @args;
279             }
280              
281             1;
282             __END__