ruleby 0.1 → 0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|