ruleby 0.3 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/core/atoms.rb +76 -27
- data/lib/core/engine.rb +11 -5
- data/lib/core/nodes.rb +175 -71
- data/lib/core/patterns.rb +21 -18
- data/lib/dsl/ferrari.rb +50 -33
- data/lib/dsl/letigre.rb +43 -17
- data/lib/dsl/steel.rb +1 -0
- data/lib/dsl/yaml_dsl.rb +23 -0
- data/lib/rulebook.rb +18 -3
- data/lib/ruleby.rb +37 -0
- data/tests/test.rb +15 -0
- metadata +53 -57
- data/benchmarks/basic_rules.rb +0 -66
- data/benchmarks/joined_rules.rb +0 -73
- data/benchmarks/miss_manners/data.rb +0 -146
- data/benchmarks/miss_manners/miss_manners.rb +0 -33
- data/benchmarks/miss_manners/model.rb +0 -193
- data/benchmarks/miss_manners/rules.rb +0 -104
- data/benchmarks/model.rb +0 -36
- data/examples/example_diagnosis.rb +0 -117
- data/examples/example_hello.rb +0 -46
- data/examples/example_politician.rb +0 -97
- data/examples/example_ticket.rb +0 -113
- data/examples/fibonacci_example1.rb +0 -44
- data/examples/fibonacci_example2.rb +0 -40
- data/examples/fibonacci_rulebook.rb +0 -84
- data/examples/test_self_reference.rb +0 -77
data/lib/core/patterns.rb
CHANGED
@@ -19,29 +19,27 @@ module Ruleby
|
|
19
19
|
# object. It contains a list of 'atoms' that represent the properties of
|
20
20
|
# the class that we are looking for.
|
21
21
|
class ObjectPattern < Pattern
|
22
|
-
|
23
|
-
attr_reader :head
|
24
22
|
attr_reader :atoms
|
25
23
|
|
26
|
-
|
27
|
-
|
28
|
-
init_vars tag, deftemplate, atoms
|
24
|
+
def initialize(head, atoms)
|
25
|
+
@atoms = [head] + atoms
|
29
26
|
end
|
30
27
|
|
31
|
-
def
|
32
|
-
@
|
33
|
-
@atoms = [@head] + atoms
|
28
|
+
def head
|
29
|
+
@atoms[0]
|
34
30
|
end
|
35
31
|
|
36
32
|
def ==(pattern)
|
37
|
-
|
38
|
-
|
39
|
-
(
|
40
|
-
|
41
|
-
|
33
|
+
if pattern.class == self.class
|
34
|
+
atoms = pattern.atoms
|
35
|
+
if(@atoms.size == atoms.size)
|
36
|
+
(0..@atoms.size).each do |i|
|
37
|
+
if !(@atoms[i] == atoms[i])
|
38
|
+
return false
|
39
|
+
end
|
42
40
|
end
|
41
|
+
return true
|
43
42
|
end
|
44
|
-
return true
|
45
43
|
end
|
46
44
|
return false
|
47
45
|
end
|
@@ -51,13 +49,16 @@ module Ruleby
|
|
51
49
|
end
|
52
50
|
end
|
53
51
|
|
52
|
+
class InheritsPattern < ObjectPattern
|
53
|
+
end
|
54
|
+
|
54
55
|
# This class represents a pattern that is looking for the absence of some
|
55
56
|
# object (rather than the existence of). In all respects, it is the same as
|
56
57
|
# an ObjectPattern, but it is handled differently by the inference engine.
|
57
58
|
class NotPattern < ObjectPattern
|
58
|
-
|
59
|
-
|
60
|
-
|
59
|
+
end
|
60
|
+
|
61
|
+
class NotInheritsPattern < InheritsPattern
|
61
62
|
end
|
62
63
|
|
63
64
|
# A composite pattern represents a logical conjunction of two patterns. The
|
@@ -99,6 +100,8 @@ module Ruleby
|
|
99
100
|
|
100
101
|
end
|
101
102
|
|
102
|
-
|
103
|
+
class PatternFactory
|
104
|
+
|
105
|
+
end
|
103
106
|
end
|
104
107
|
end
|
data/lib/dsl/ferrari.rb
CHANGED
@@ -44,47 +44,52 @@ module Ruleby
|
|
44
44
|
@name = name
|
45
45
|
@pattern = pattern
|
46
46
|
@action = action
|
47
|
-
@priority = priority
|
47
|
+
@priority = priority
|
48
48
|
|
49
49
|
@tags = {}
|
50
50
|
@methods = {}
|
51
51
|
@when_counter = 0
|
52
52
|
end
|
53
53
|
|
54
|
-
def when(*args)
|
55
|
-
clazz = args.shift
|
54
|
+
def when(*args)
|
55
|
+
clazz = AtomBuilder === args[0] ? nil : args.shift
|
56
56
|
is_not = false
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
57
|
+
mode = :equals
|
58
|
+
while clazz.is_a? Symbol
|
59
|
+
if clazz == :not || clazz == :~
|
60
|
+
is_not = true
|
61
|
+
elsif clazz == :is_a? || clazz == :kind_of? || clazz == :instance_of?
|
62
|
+
mode = :inherits
|
63
|
+
elsif clazz == :exists?
|
64
|
+
raise 'The \'exists\' quantifier is not yet supported.'
|
65
|
+
end
|
66
|
+
clazz = args.empty? ? nil : args.shift
|
67
|
+
end
|
68
|
+
|
69
|
+
if clazz == nil
|
70
|
+
clazz = Object
|
71
|
+
mode = :inherits
|
62
72
|
end
|
63
73
|
|
64
|
-
|
74
|
+
deftemplate = Core::DefTemplate.new clazz, mode
|
65
75
|
atoms = []
|
66
76
|
@when_counter += 1
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
@tags[head_tag] = @when_counter
|
71
|
-
else
|
72
|
-
head_tag = GeneratedTag.new
|
73
|
-
@tags[head_tag] = @when_counter
|
74
|
-
end
|
77
|
+
htag = Symbol === args[0] ? args.shift : GeneratedTag.new
|
78
|
+
head = Core::HeadAtom.new htag, deftemplate
|
79
|
+
@tags[htag] = @when_counter
|
75
80
|
|
76
81
|
args.each do |arg|
|
77
82
|
if arg.kind_of? Hash
|
78
83
|
arg.each do |ab,tag|
|
79
84
|
ab.tag = tag
|
80
|
-
ab.
|
85
|
+
ab.deftemplate = deftemplate
|
81
86
|
@tags[tag] = @when_counter
|
82
87
|
@methods[tag] = ab.name
|
83
88
|
atoms.push ab.build_atom(@tags, @methods, @when_counter)
|
84
89
|
end
|
85
90
|
elsif arg.kind_of? AtomBuilder
|
86
91
|
arg.tag = GeneratedTag.new
|
87
|
-
arg.
|
92
|
+
arg.deftemplate = deftemplate
|
88
93
|
@methods[arg.tag] = arg.name
|
89
94
|
atoms.push arg.build_atom(@tags, @methods, @when_counter)
|
90
95
|
elsif arg == false
|
@@ -94,8 +99,13 @@ module Ruleby
|
|
94
99
|
end
|
95
100
|
end
|
96
101
|
|
97
|
-
|
98
|
-
|
102
|
+
if is_not
|
103
|
+
p = mode==:inherits ? Core::NotInheritsPattern.new(head, atoms) :
|
104
|
+
Core::NotPattern.new(head, atoms)
|
105
|
+
else
|
106
|
+
p = mode==:inherits ? Core::InheritsPattern.new(head, atoms) :
|
107
|
+
Core::ObjectPattern.new(head, atoms)
|
108
|
+
end
|
99
109
|
@pattern = @pattern ? Core::AndPattern.new(@pattern, p) : p
|
100
110
|
|
101
111
|
return nil
|
@@ -171,11 +181,11 @@ module Ruleby
|
|
171
181
|
end
|
172
182
|
|
173
183
|
class AtomBuilder
|
174
|
-
attr_accessor :tag, :name, :bindings, :
|
184
|
+
attr_accessor :tag, :name, :bindings, :deftemplate, :block
|
175
185
|
|
176
186
|
def initialize(name)
|
177
187
|
@name = name
|
178
|
-
@
|
188
|
+
@deftemplate = nil
|
179
189
|
@tag = nil
|
180
190
|
@bindings = []
|
181
191
|
@block = lambda {|x| true}
|
@@ -188,6 +198,8 @@ module Ruleby
|
|
188
198
|
end
|
189
199
|
|
190
200
|
def ==(value)
|
201
|
+
@atom_type = :equals
|
202
|
+
@value = value
|
191
203
|
create_block value, lambda {|x,y| x == y}, lambda {|x| x == value}; self
|
192
204
|
end
|
193
205
|
|
@@ -209,22 +221,27 @@ module Ruleby
|
|
209
221
|
|
210
222
|
def >=(value)
|
211
223
|
create_block value, lambda {|x,y| x >= y}, lambda {|x| x >= value}; self
|
212
|
-
end
|
224
|
+
end
|
213
225
|
|
214
|
-
def
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
226
|
+
def =~(value)
|
227
|
+
create_block value, lambda {|x,y| x =~ y}, lambda {|x| x =~ value}; self
|
228
|
+
end
|
229
|
+
|
230
|
+
def build_atom(tags,methods,when_id)
|
231
|
+
if @bindings.empty?
|
232
|
+
if @atom_type == :equals
|
233
|
+
return Core::EqualsAtom.new(@tag, @name, @deftemplate, @value)
|
234
|
+
else
|
235
|
+
return Core::PropertyAtom.new(@tag, @name, @deftemplate, &@block)
|
236
|
+
end
|
237
|
+
end
|
221
238
|
|
222
239
|
if references_self?(tags,when_id)
|
223
240
|
bind_methods = @bindings.collect{ |bb| methods[bb.tag] }
|
224
|
-
Core::SelfReferenceAtom.new(@tag,@name,bind_methods,@
|
241
|
+
Core::SelfReferenceAtom.new(@tag,@name,bind_methods,@deftemplate,&@block)
|
225
242
|
else
|
226
243
|
bind_tags = @bindings.collect{ |bb| bb.tag }
|
227
|
-
Core::ReferenceAtom.new(@tag,@name,bind_tags,@
|
244
|
+
Core::ReferenceAtom.new(@tag,@name,bind_tags,@deftemplate,&@block)
|
228
245
|
end
|
229
246
|
end
|
230
247
|
|
data/lib/dsl/letigre.rb
CHANGED
@@ -13,7 +13,8 @@ module Ruleby
|
|
13
13
|
module LeTigre
|
14
14
|
class RulebookHelper
|
15
15
|
|
16
|
-
def initialize(engine)
|
16
|
+
def initialize(engine, rulebook)
|
17
|
+
@rulebook = rulebook
|
17
18
|
@engine = engine
|
18
19
|
end
|
19
20
|
|
@@ -26,7 +27,7 @@ module Ruleby
|
|
26
27
|
|
27
28
|
options = args[0].kind_of?(Hash) ? args.shift : {}
|
28
29
|
|
29
|
-
pb = PatternParser.new
|
30
|
+
pb = PatternParser.new @rulebook
|
30
31
|
pattern = pb.parse args
|
31
32
|
|
32
33
|
rb = RuleBuilder.new name
|
@@ -81,14 +82,18 @@ module Ruleby
|
|
81
82
|
@@method_error = "No #method in expression: "
|
82
83
|
|
83
84
|
@@base_re = /(For each|\w*\s*exists\??|not\??)(.*)/
|
84
|
-
@@
|
85
|
+
@@mode_re = /( (is a|kind of|instance of) )(.*)/
|
86
|
+
@@where_re = /(.*)where (.*)/
|
85
87
|
@@head_re = /(\w*)( as :(.*))?/
|
86
88
|
|
87
|
-
@@method_re = /#(\w
|
89
|
+
@@method_re = /#((\w|\d|\_)*)\??/
|
88
90
|
@@bind_re = /#:(\w*|\d*\_*)*/
|
89
91
|
@@and_re = /#&&/
|
90
92
|
@@tag_re = /(.*) as :(.*)/
|
91
93
|
|
94
|
+
def initialize(rulebook)
|
95
|
+
@rulebook = rulebook
|
96
|
+
end
|
92
97
|
|
93
98
|
def parse(lhs_strs)
|
94
99
|
pattern = nil
|
@@ -104,6 +109,13 @@ module Ruleby
|
|
104
109
|
|
105
110
|
raise 'The \'exists\' quantifier is not yet supported.' if base =~ /exists/
|
106
111
|
|
112
|
+
if tail =~ @@mode_re
|
113
|
+
mode = :inherits
|
114
|
+
tail = $3
|
115
|
+
else
|
116
|
+
mode = :equals
|
117
|
+
end
|
118
|
+
|
107
119
|
# check if there is a where clause
|
108
120
|
if tail =~ @@where_re
|
109
121
|
head = $1.strip
|
@@ -114,27 +126,41 @@ module Ruleby
|
|
114
126
|
end
|
115
127
|
|
116
128
|
# match the class type and tag
|
117
|
-
|
118
|
-
|
119
|
-
|
129
|
+
if head != ''
|
130
|
+
head =~ @@head_re
|
131
|
+
clazz = @rulebook.__eval__ $1
|
132
|
+
tag = $3 ? $3.to_sym : GeneratedTag.new
|
133
|
+
else
|
134
|
+
clazz = Object
|
135
|
+
tag = GeneratedTag.new
|
136
|
+
mode = :inherits
|
137
|
+
end
|
138
|
+
|
139
|
+
deftemplate = Core::DefTemplate.new clazz, mode
|
140
|
+
head = Core::HeadAtom.new tag, deftemplate
|
120
141
|
|
121
142
|
atoms = []
|
122
143
|
atom_strs = tail ? tail.split(@@and_re) : []
|
123
144
|
atom_strs.each do |a|
|
124
145
|
# BUG we also need to pass in the head_tag with atoms!
|
125
|
-
atoms.push parse_atom(a,
|
146
|
+
atoms.push parse_atom(a, deftemplate, atoms)
|
126
147
|
end
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
148
|
+
|
149
|
+
if base =~ /not\??/
|
150
|
+
p = mode==:inherits ? Core::NotInheritsPattern.new(head, atoms) :
|
151
|
+
Core::NotPattern.new(head, atoms)
|
152
|
+
else
|
153
|
+
p = mode==:inherits ? Core::InheritsPattern.new(head, atoms) :
|
154
|
+
Core::ObjectPattern.new(head, atoms)
|
155
|
+
end
|
156
|
+
|
131
157
|
pattern = pattern ? Core::AndPattern.new(pattern, p) : p
|
132
158
|
end
|
133
159
|
return pattern
|
134
160
|
end
|
135
161
|
|
136
162
|
private
|
137
|
-
def parse_atom(str,
|
163
|
+
def parse_atom(str, deftemplate, atoms)
|
138
164
|
expression, tag = nil, nil
|
139
165
|
if str =~ @@tag_re
|
140
166
|
expression, tag = $1, $2.strip.to_sym
|
@@ -169,12 +195,12 @@ module Ruleby
|
|
169
195
|
block = eval proc
|
170
196
|
|
171
197
|
if bindings.empty?
|
172
|
-
return Core::PropertyAtom.new(tag, method,
|
198
|
+
return Core::PropertyAtom.new(tag, method, deftemplate, &block)
|
173
199
|
elsif references_self?(bindings, atoms)
|
174
200
|
bound_methods = resolve_bindings(bindings, atoms)
|
175
|
-
return Core::SelfReferenceAtom.new(tag, method, bound_methods,
|
201
|
+
return Core::SelfReferenceAtom.new(tag, method, bound_methods, deftemplate, &block)
|
176
202
|
else
|
177
|
-
return Core::ReferenceAtom.new(tag, method, bindings,
|
203
|
+
return Core::ReferenceAtom.new(tag, method, bindings, deftemplate, &block)
|
178
204
|
end
|
179
205
|
end
|
180
206
|
|
@@ -200,7 +226,7 @@ module Ruleby
|
|
200
226
|
bindings.each do |b|
|
201
227
|
atoms.each do |a|
|
202
228
|
if a.tag == b
|
203
|
-
bound_methods.push a.
|
229
|
+
bound_methods.push a.method
|
204
230
|
end
|
205
231
|
end
|
206
232
|
end
|
data/lib/dsl/steel.rb
CHANGED
data/lib/dsl/yaml_dsl.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
|
2
|
+
require 'ruleby'
|
3
|
+
require 'dsl/letigre'
|
4
|
+
module Ruleby
|
5
|
+
module YamlDsl
|
6
|
+
require 'yaml'
|
7
|
+
def self.load_rules(rules_yaml, engine)
|
8
|
+
ry = YAML::load(rules_yaml)
|
9
|
+
ry.each do |k,v|
|
10
|
+
if k =~ /_rule/
|
11
|
+
wh = v['when']
|
12
|
+
wh.gsub!('@', '#')
|
13
|
+
wh = wh.split(',')
|
14
|
+
th = v['then']
|
15
|
+
priority = v['priority']
|
16
|
+
th = "context engine -> #{th}"
|
17
|
+
r = LeTigre::RulebookHelper.new engine
|
18
|
+
r.rule k, { :priority => priority }, *wh, &th.to_proc
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/rulebook.rb
CHANGED
@@ -17,12 +17,23 @@ require 'dsl/steel'
|
|
17
17
|
module Ruleby
|
18
18
|
class Rulebook
|
19
19
|
include Ruleby
|
20
|
-
def initialize(engine)
|
20
|
+
def initialize(engine, session={})
|
21
21
|
@engine = engine
|
22
|
+
@session = session
|
22
23
|
end
|
23
24
|
|
24
25
|
attr_reader :engine
|
25
|
-
|
26
|
+
attr_reader :session
|
27
|
+
|
28
|
+
def assert(fact)
|
29
|
+
@engine.assert fact
|
30
|
+
end
|
31
|
+
def retract(fact)
|
32
|
+
@engine.retract fact
|
33
|
+
end
|
34
|
+
def modify(fact)
|
35
|
+
@engine.modify fact
|
36
|
+
end
|
26
37
|
def rule(*args, &block)
|
27
38
|
unless args.empty?
|
28
39
|
name = args[0].kind_of?(Symbol) ? args.shift : GeneratedTag.new
|
@@ -40,7 +51,7 @@ module Ruleby
|
|
40
51
|
r.rule name, *args, &block
|
41
52
|
elsif args[i].kind_of? String
|
42
53
|
# use letigre DSL
|
43
|
-
r = LeTigre::RulebookHelper.new @engine
|
54
|
+
r = LeTigre::RulebookHelper.new @engine, self
|
44
55
|
r.rule name, *args, &block
|
45
56
|
else
|
46
57
|
raise 'Rule format not recognized.'
|
@@ -71,6 +82,10 @@ module Ruleby
|
|
71
82
|
def condition(&block)
|
72
83
|
return lambda(&block)
|
73
84
|
end
|
85
|
+
|
86
|
+
def __eval__(x)
|
87
|
+
eval(x)
|
88
|
+
end
|
74
89
|
end
|
75
90
|
|
76
91
|
class GeneratedTag
|
data/lib/ruleby.rb
CHANGED
@@ -19,4 +19,41 @@ module Ruleby
|
|
19
19
|
yield e if block_given?
|
20
20
|
return e
|
21
21
|
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class String
|
25
|
+
unless ''.respond_to?(:to_proc)
|
26
|
+
def to_proc &block
|
27
|
+
params = []
|
28
|
+
expr = self
|
29
|
+
sections = expr.split(/\s*->\s*/m)
|
30
|
+
if sections.length > 1 then
|
31
|
+
eval sections.reverse!.inject { |e, p| "(Proc.new { |#{p.split(/\s/).join(', ')}| #{e} })" }, block && block.binding
|
32
|
+
elsif expr.match(/\b_\b/)
|
33
|
+
eval "Proc.new { |_| #{expr} }", block && block.binding
|
34
|
+
else
|
35
|
+
leftSection = expr.match(/^\s*(?:[+*\/%&|\^\.=<>\[]|!=)/m)
|
36
|
+
rightSection = expr.match(/[+\-*\/%&|\^\.=<>!]\s*$/m)
|
37
|
+
if leftSection || rightSection then
|
38
|
+
if (leftSection) then
|
39
|
+
params.push('$left')
|
40
|
+
expr = '$left' + expr
|
41
|
+
end
|
42
|
+
if (rightSection) then
|
43
|
+
params.push('$right')
|
44
|
+
expr = expr + '$right'
|
45
|
+
end
|
46
|
+
else
|
47
|
+
self.gsub(
|
48
|
+
/(?:\b[A-Z]|\.[a-zA-Z_$])[a-zA-Z_$\d]*|[a-zA-Z_$][a-zA-Z_$\d]*:|self|arguments|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/, ''
|
49
|
+
).scan(
|
50
|
+
/([a-z_$][a-z_$\d]*)/i
|
51
|
+
) do |v|
|
52
|
+
params.push(v) unless params.include?(v)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
eval "Proc.new { |#{params.join(', ')}| #{expr} }", block && block.binding
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
22
59
|
end
|