ruleby 0.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/benchmarks/50_joined_rules.rb +78 -0
- data/benchmarks/50_rules.rb +57 -0
- data/benchmarks/5_joined_rules.rb +78 -0
- data/benchmarks/5_rules.rb +57 -0
- data/benchmarks/miss_manners/data.rb +135 -0
- data/benchmarks/miss_manners/miss_manners.rb +23 -0
- data/benchmarks/miss_manners/model.rb +182 -0
- data/benchmarks/miss_manners/rules.rb +165 -0
- data/examples/example_diagnosis.rb +155 -0
- data/examples/example_hello.rb +48 -0
- data/examples/example_politician.rb +86 -0
- data/examples/example_ticket.rb +158 -0
- data/examples/fibonacci_example1.rb +33 -0
- data/examples/fibonacci_example2.rb +29 -0
- data/examples/fibonacci_rulebook.rb +137 -0
- data/examples/test_self_reference.rb +35 -0
- data/lib/core/atoms.rb +13 -71
- data/lib/core/engine.rb +47 -9
- data/lib/core/nodes.rb +487 -548
- data/lib/core/patterns.rb +0 -110
- data/lib/core/utils.rb +120 -184
- data/lib/rulebook.rb +237 -157
- data/lib/ruleby.rb +0 -20
- metadata +28 -11
@@ -0,0 +1,158 @@
|
|
1
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/')
|
2
|
+
require 'ruleby'
|
3
|
+
require 'rulebook'
|
4
|
+
|
5
|
+
# NOTE : before we can get this example working we need to figure out exactly
|
6
|
+
# what the conflict resolution strategy should be. When we implement the
|
7
|
+
# default strategy as we understand it (from various literature) this example
|
8
|
+
# does not work (nor should it). But this example works in drools as is. Why?
|
9
|
+
|
10
|
+
class Customer
|
11
|
+
def initialize(name,subscription)
|
12
|
+
@name = name
|
13
|
+
@subscription = subscription
|
14
|
+
end
|
15
|
+
attr_reader :name,:subscription
|
16
|
+
def to_s
|
17
|
+
return '[Customer ' + @name.to_s + ' : ' + @subscription.to_s + ']';
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Ticket
|
22
|
+
def initialize(customer)
|
23
|
+
@customer = customer
|
24
|
+
@status = 'New'
|
25
|
+
end
|
26
|
+
attr :status, true
|
27
|
+
attr_reader :customer
|
28
|
+
def to_s
|
29
|
+
return '[Ticket ' + @customer.to_s + ' : ' + @status.to_s + ']';
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# This example is used in JBoss-Rules to demonstrate durations and the use of
|
34
|
+
# custom DSL. We are simply using here to demonstrate another example.
|
35
|
+
class TroubleTicketRulebook < Rulebook
|
36
|
+
def rules
|
37
|
+
rule 'New Ticket' do |r|
|
38
|
+
r.when do |has|
|
39
|
+
has.customer Customer
|
40
|
+
has.ticket Ticket
|
41
|
+
has.ticket.customer = :customer, :%
|
42
|
+
has.ticket.status = 'New'
|
43
|
+
end
|
44
|
+
|
45
|
+
r.then = action do |e,vars|
|
46
|
+
puts 'New : ' + vars[:ticket].to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
r.priority = 10
|
50
|
+
#r.duration = 10
|
51
|
+
end
|
52
|
+
|
53
|
+
rule 'Silver Priority' do |r|
|
54
|
+
r.when do |has|
|
55
|
+
has.customer Customer
|
56
|
+
has.customer.subscription = 'Silver'
|
57
|
+
|
58
|
+
has.ticket Ticket
|
59
|
+
has.ticket.customer = :customer, :%
|
60
|
+
has.ticket.status = 'New'
|
61
|
+
end
|
62
|
+
|
63
|
+
r.then = action do |e,vars|
|
64
|
+
vars[:ticket].status = 'Escalate'
|
65
|
+
e.modify vars[:ticket]
|
66
|
+
end
|
67
|
+
#r.duration = 3000
|
68
|
+
end
|
69
|
+
|
70
|
+
rule 'Gold Priority' do |r|
|
71
|
+
r.when do |has|
|
72
|
+
has.customer Customer
|
73
|
+
has.customer.subscription = 'Gold'
|
74
|
+
|
75
|
+
has.ticket Ticket
|
76
|
+
has.ticket.customer = :customer, :%
|
77
|
+
has.ticket.status = 'New'
|
78
|
+
end
|
79
|
+
|
80
|
+
r.then = action do |e,vars|
|
81
|
+
vars[:ticket].status = 'Escalate'
|
82
|
+
e.modify vars[:ticket]
|
83
|
+
end
|
84
|
+
#r.duration = 1000
|
85
|
+
end
|
86
|
+
|
87
|
+
rule 'Platinum Priority' do |r|
|
88
|
+
r.when do |has|
|
89
|
+
has.customer Customer
|
90
|
+
has.customer.subscription = 'Platinum'
|
91
|
+
|
92
|
+
has.ticket Ticket
|
93
|
+
has.ticket.customer = :customer, :%
|
94
|
+
has.ticket.status = 'New'
|
95
|
+
end
|
96
|
+
|
97
|
+
r.then = action do |e,vars|
|
98
|
+
vars[:ticket].status = 'Escalate'
|
99
|
+
e.modify vars[:ticket]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
rule 'Escalate' do |r|
|
104
|
+
r.when do |has|
|
105
|
+
has.customer Customer
|
106
|
+
|
107
|
+
has.ticket Ticket
|
108
|
+
has.ticket.customer = :customer, :%
|
109
|
+
has.ticket.status = 'Escalate'
|
110
|
+
end
|
111
|
+
|
112
|
+
r.then = action do |e,vars|
|
113
|
+
puts 'Email : ' + vars[:ticket].to_s
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
rule 'Done' do |r|
|
118
|
+
r.when do |has|
|
119
|
+
has.customer Customer
|
120
|
+
|
121
|
+
has.ticket Ticket
|
122
|
+
has.ticket.customer = :customer, :%
|
123
|
+
has.ticket.status = 'Done'
|
124
|
+
end
|
125
|
+
|
126
|
+
r.then = action do |e,vars|
|
127
|
+
puts 'Done : ' + vars[:ticket].to_s
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
include Ruleby
|
134
|
+
|
135
|
+
# FACTS
|
136
|
+
|
137
|
+
a = Customer.new('A', 'Gold')
|
138
|
+
b = Customer.new('B', 'Platinum')
|
139
|
+
c = Customer.new('C', 'Silver')
|
140
|
+
d = Customer.new('D', 'Silver')
|
141
|
+
|
142
|
+
t1 = Ticket.new(a)
|
143
|
+
t2 = Ticket.new(b)
|
144
|
+
t3 = Ticket.new(c)
|
145
|
+
t4 = Ticket.new(d)
|
146
|
+
|
147
|
+
engine :engine do |e|
|
148
|
+
TroubleTicketRulebook.new(e).rules
|
149
|
+
e.assert a
|
150
|
+
e.assert b
|
151
|
+
e.assert c
|
152
|
+
e.assert d
|
153
|
+
e.assert t1
|
154
|
+
e.assert t2
|
155
|
+
e.assert t3
|
156
|
+
e.assert t4
|
157
|
+
e.match
|
158
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/')
|
2
|
+
require 'ruleby'
|
3
|
+
require 'fibonacci_rulebook'
|
4
|
+
class Fibonacci
|
5
|
+
def initialize(sequence,value=-1)
|
6
|
+
@sequence = sequence
|
7
|
+
@value = value
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :sequence
|
11
|
+
attr :value, true
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
return '['+super + " sequence=" + @sequence.to_s + ",value=" + @value.to_s + ']'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
include Ruleby
|
19
|
+
|
20
|
+
# This example is borrowed from the JBoss-Rule project.
|
21
|
+
|
22
|
+
# FACTS
|
23
|
+
fib1 = Fibonacci.new(8)
|
24
|
+
|
25
|
+
t1 = Time.new
|
26
|
+
engine :engine do |e|
|
27
|
+
FibonacciRulebook1.new(e).rules
|
28
|
+
e.assert fib1
|
29
|
+
e.match
|
30
|
+
end
|
31
|
+
t2 = Time.new
|
32
|
+
diff = t2.to_f - t1.to_f
|
33
|
+
puts diff.to_s
|
@@ -0,0 +1,29 @@
|
|
1
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/')
|
2
|
+
require 'ruleby'
|
3
|
+
require 'fibonacci_rulebook'
|
4
|
+
class Fibonacci
|
5
|
+
def initialize(sequence,value=-1)
|
6
|
+
@sequence = sequence
|
7
|
+
@value = value
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :sequence
|
11
|
+
attr :value, true
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
return super + "::sequence=" + @sequence.to_s + ",value=" + @value.to_s
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
include Ruleby
|
19
|
+
|
20
|
+
# FACTS
|
21
|
+
fib1 = Fibonacci.new(1,1)
|
22
|
+
fib2 = Fibonacci.new(2,1)
|
23
|
+
|
24
|
+
engine :engine do |e|
|
25
|
+
FibonacciRulebook2.new(e).rules
|
26
|
+
e.assert fib1
|
27
|
+
e.assert fib2
|
28
|
+
e.match
|
29
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
|
2
|
+
require 'rulebook'
|
3
|
+
class FibonacciRulebook2 < Rulebook
|
4
|
+
MAX_SEQUENCE = 100
|
5
|
+
def rules
|
6
|
+
rule 'Calculate' do |r|
|
7
|
+
|
8
|
+
r.when do |has|
|
9
|
+
has.f1 Fibonacci
|
10
|
+
has.f1.value.not = -1
|
11
|
+
has.f1.sequence :s1
|
12
|
+
|
13
|
+
has.f2 Fibonacci
|
14
|
+
has.f2.value.not = -1
|
15
|
+
has.f2.sequence :s2
|
16
|
+
has.f2.sequence :s1 do |s, s1| s == (s1 + 1) end
|
17
|
+
|
18
|
+
has.f3 Fibonacci
|
19
|
+
has.f3.value = -1
|
20
|
+
has.f3.sequence :s2 do |s, s2| s == (s2 + 1) end
|
21
|
+
end
|
22
|
+
|
23
|
+
r.then do |e, vars|
|
24
|
+
e.retract vars[:f1]
|
25
|
+
e.retract vars[:f3]
|
26
|
+
if(vars[:f2].sequence == MAX_SEQUENCE)
|
27
|
+
e.retract vars[:f2]
|
28
|
+
else
|
29
|
+
f3 = Fibonacci.new(vars[:f2].sequence + 1, vars[:f1].value + vars[:f2].value)
|
30
|
+
e.assert f3
|
31
|
+
puts "#{f3.sequence} == #{f3.value}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
r.priority = 2
|
36
|
+
end
|
37
|
+
|
38
|
+
rule 'Build' do |r|
|
39
|
+
|
40
|
+
r.when do |has|
|
41
|
+
has.f Fibonacci
|
42
|
+
has.f.value.not = -1
|
43
|
+
has.f.sequence :s1
|
44
|
+
|
45
|
+
has.f2 Fibonacci
|
46
|
+
has.f2.value.not = -1
|
47
|
+
has.f2.sequence :s1 do |s, s1| s == (s1 + 1) end
|
48
|
+
end
|
49
|
+
|
50
|
+
r.then do |e, vars|
|
51
|
+
f3 = Fibonacci.new(vars[:f2].sequence + 1, -1)
|
52
|
+
e.assert f3
|
53
|
+
end
|
54
|
+
|
55
|
+
r.priority = 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class FibonacciRulebook1 < Rulebook
|
61
|
+
|
62
|
+
def rules
|
63
|
+
# Bootstrap1
|
64
|
+
rule 'Bootstrap1' do |r|
|
65
|
+
r.when do |has|
|
66
|
+
has.f Fibonacci
|
67
|
+
has.f.value = -1
|
68
|
+
has.f.sequence = 1
|
69
|
+
end
|
70
|
+
|
71
|
+
r.then do |e,vars|
|
72
|
+
vars[:f].value = 1
|
73
|
+
e.modify vars[:f]
|
74
|
+
puts vars[:f].sequence.to_s + ' == ' + vars[:f].value.to_s
|
75
|
+
end
|
76
|
+
|
77
|
+
r.priority = 4
|
78
|
+
end
|
79
|
+
|
80
|
+
# Recurse
|
81
|
+
rule 'Recurse' do |r|
|
82
|
+
r.when do |has|
|
83
|
+
has.f Fibonacci
|
84
|
+
has.f.value = -1
|
85
|
+
end
|
86
|
+
|
87
|
+
r.then do |e,vars|
|
88
|
+
f2 = Fibonacci.new(vars[:f].sequence - 1)
|
89
|
+
e.assert f2
|
90
|
+
puts 'recurse for ' + f2.sequence.to_s
|
91
|
+
end
|
92
|
+
r.priority = 3
|
93
|
+
end
|
94
|
+
|
95
|
+
# Bootstrap2
|
96
|
+
rule 'Bootstrap2' do |r|
|
97
|
+
r.when do |has|
|
98
|
+
has.f9 Fibonacci
|
99
|
+
has.f9.value = -1
|
100
|
+
has.f9.sequence = 2
|
101
|
+
end
|
102
|
+
r.then do |e,vars|
|
103
|
+
vars[:f9].value = 1
|
104
|
+
e.modify vars[:f9]
|
105
|
+
puts vars[:f9].sequence.to_s + ' == ' + vars[:f9].value.to_s
|
106
|
+
end
|
107
|
+
r.priority = 0
|
108
|
+
end
|
109
|
+
|
110
|
+
# Calculate
|
111
|
+
rule 'Calculate' do |r|
|
112
|
+
r.when do |has|
|
113
|
+
has.f1 Fibonacci
|
114
|
+
has.f1.value.not = -1
|
115
|
+
has.f1.sequence :s1
|
116
|
+
|
117
|
+
has.f2 Fibonacci
|
118
|
+
has.f2.value.not = -1
|
119
|
+
has.f2.sequence :s2
|
120
|
+
has.f2.sequence.references :s1
|
121
|
+
has.f2.sequence do |s2, s1| s2 == (s1 + 1) end
|
122
|
+
|
123
|
+
has.f3 Fibonacci
|
124
|
+
has.f3.value = -1
|
125
|
+
has.f3.sequence.references :s2
|
126
|
+
has.f3.sequence do |sequence, s2| sequence == (s2 + 1) end
|
127
|
+
end
|
128
|
+
r.then do |e,vars|
|
129
|
+
vars[:f3].value = vars[:f1].value + vars[:f2].value
|
130
|
+
e.modify vars[:f3]
|
131
|
+
e.retract vars[:f1]
|
132
|
+
puts vars[:f3].sequence.to_s + ' == ' + vars[:f3].value.to_s
|
133
|
+
end
|
134
|
+
r.priority = 0
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/')
|
2
|
+
require 'ruleby'
|
3
|
+
require 'rulebook'
|
4
|
+
|
5
|
+
class Message
|
6
|
+
def initialize(status,message)
|
7
|
+
@status = status
|
8
|
+
@message = message
|
9
|
+
end
|
10
|
+
attr :status, true
|
11
|
+
attr :message, true
|
12
|
+
end
|
13
|
+
|
14
|
+
class SelfRefRulebook < Rulebook
|
15
|
+
def rules
|
16
|
+
rule 'HelloWorld' do |r|
|
17
|
+
r.when do |has|
|
18
|
+
has.m2 Message
|
19
|
+
has.m2.message :x
|
20
|
+
has.m2.status = :x, :%
|
21
|
+
end
|
22
|
+
r.then = action do |e,vars|
|
23
|
+
puts 'Success'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
include Ruleby
|
30
|
+
|
31
|
+
engine :engine do |e|
|
32
|
+
SelfRefRulebook.new(e).rules
|
33
|
+
e.assert Message.new(:HELLO, :HELLO)
|
34
|
+
e.match
|
35
|
+
end
|
data/lib/core/atoms.rb
CHANGED
@@ -11,14 +11,11 @@ module Ruleby
|
|
11
11
|
@tag = tag
|
12
12
|
@name = name
|
13
13
|
@proc = Proc.new(&block) if block_given?
|
14
|
-
@parent = nil # TODO move to AtomNode
|
15
14
|
end
|
16
15
|
|
17
|
-
attr :parent, true
|
18
16
|
attr_reader :name
|
19
17
|
attr_reader :tag
|
20
18
|
attr_reader :proc
|
21
|
-
|
22
19
|
end
|
23
20
|
|
24
21
|
# This kind of atom is used to match just a single, hard coded value.
|
@@ -27,28 +24,15 @@ module Ruleby
|
|
27
24
|
# a.name do |n| n == 'John' end
|
28
25
|
#
|
29
26
|
# So there are no references to other atoms.
|
30
|
-
class PropertyAtom < Atom
|
31
|
-
|
32
|
-
def match?(fact)
|
33
|
-
mr = MatchResult.new
|
34
|
-
temp = fact.object.send("#{@name}")
|
35
|
-
if @proc.call(temp)
|
36
|
-
mr.is_match = true
|
37
|
-
mr.fact_hash[@tag] = fact.id
|
38
|
-
mr.recency.push fact.recency
|
39
|
-
mr[@tag] = temp
|
40
|
-
end
|
41
|
-
return [mr]
|
42
|
-
end
|
43
|
-
|
44
|
-
def matches
|
45
|
-
@parent.obj_results
|
46
|
-
end
|
47
|
-
|
27
|
+
class PropertyAtom < Atom
|
48
28
|
def ==(atom)
|
49
|
-
return atom
|
29
|
+
return shareable?(atom) && @tag == atom.tag
|
50
30
|
end
|
51
31
|
|
32
|
+
def shareable?(atom)
|
33
|
+
return atom && @name == atom.name && @proc == atom.proc
|
34
|
+
end
|
35
|
+
|
52
36
|
def to_s
|
53
37
|
return 'tag='+@tag.to_s + '[' + matches.join(',') + ']'
|
54
38
|
end
|
@@ -60,17 +44,7 @@ module Ruleby
|
|
60
44
|
#
|
61
45
|
# It is only used at the start of a pattern.
|
62
46
|
class TypeAtom < PropertyAtom
|
63
|
-
|
64
|
-
mr = MatchResult.new
|
65
|
-
temp = fact.object.send("#{@name}")
|
66
|
-
if @proc.call(temp)
|
67
|
-
mr.is_match = true
|
68
|
-
mr.recency.push fact.recency
|
69
|
-
mr.fact_hash[@tag] = fact.id
|
70
|
-
mr[@tag] = fact.object
|
71
|
-
end
|
72
|
-
return [mr]
|
73
|
-
end
|
47
|
+
|
74
48
|
end
|
75
49
|
|
76
50
|
# This kind of atom is used for matching a value that is a variable.
|
@@ -84,46 +58,9 @@ module Ruleby
|
|
84
58
|
def initialize(tag, name, vars, &block)
|
85
59
|
super(tag, name, &block)
|
86
60
|
@vars = vars # list of referenced variable names
|
87
|
-
@var_atoms = Hash.new # will be set when rule is asserted
|
88
61
|
end
|
89
62
|
|
90
63
|
attr_reader :vars
|
91
|
-
attr :var_atoms, true
|
92
|
-
|
93
|
-
def match?(fact)
|
94
|
-
local_matches = Array.new
|
95
|
-
val = fact.object.send("#{@name}")
|
96
|
-
@vars.each do |v|
|
97
|
-
# TODO we don't really need to iterate over all the @var_atoms, just
|
98
|
-
# the one that is at the top of the reference chain (i.e. the one that
|
99
|
-
# has references to all of this atoms other references.
|
100
|
-
@var_atoms[v].matches.each do |match|
|
101
|
-
args = [val]
|
102
|
-
args.push match.variables[v]
|
103
|
-
i = 0; while i < @vars.length
|
104
|
-
if match.key? @vars[i]
|
105
|
-
args.push match.variables[@vars[i]]
|
106
|
-
i+=1
|
107
|
-
else
|
108
|
-
i = @vars.length + 1
|
109
|
-
end
|
110
|
-
end
|
111
|
-
break if i > @vars.length
|
112
|
-
if @proc.call(args)
|
113
|
-
m = MatchResult.new(match.variables.clone, true, match.fact_hash.clone, match.recency)
|
114
|
-
m.recency.push fact.recency
|
115
|
-
m.fact_hash[@tag] = fact.id
|
116
|
-
m.variables[@tag] = val
|
117
|
-
local_matches.push(m)
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
return local_matches.uniq
|
122
|
-
end
|
123
|
-
|
124
|
-
def matches
|
125
|
-
return @parent.ref_results(@tag)
|
126
|
-
end
|
127
64
|
|
128
65
|
def ==(atom)
|
129
66
|
return atom.kind_of?(ReferenceAtom) && @proc == atom.proc && @tag == atom.tag && @vars == atom.vars
|
@@ -134,6 +71,11 @@ module Ruleby
|
|
134
71
|
end
|
135
72
|
end
|
136
73
|
|
74
|
+
# This is an atom that references another atom that is in the same pattern.
|
75
|
+
class SelfReferenceAtom < ReferenceAtom
|
76
|
+
|
77
|
+
end
|
78
|
+
|
137
79
|
class MatchResult
|
138
80
|
|
139
81
|
# TODO this class needs to be cleaned up for that we don't have a bunch of
|
@@ -184,7 +126,7 @@ module Ruleby
|
|
184
126
|
@is_match = mr.is_match
|
185
127
|
@variables = @variables.update mr.variables
|
186
128
|
|
187
|
-
# QUESTION why the heck
|
129
|
+
# QUESTION why the heck does this statement work instead of the
|
188
130
|
# commented one below it??
|
189
131
|
@fact_hash = mr.fact_hash.update @fact_hash
|
190
132
|
#@fact_hash = @fact_hash.update mr.fact_hash
|