ruleby 0.3 → 0.4
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/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
|