ruleby 0.2 → 0.3
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/basic_rules.rb +66 -0
- data/benchmarks/joined_rules.rb +73 -0
- data/benchmarks/miss_manners/data.rb +11 -0
- data/benchmarks/miss_manners/miss_manners.rb +11 -1
- data/benchmarks/miss_manners/model.rb +11 -0
- data/benchmarks/miss_manners/rules.rb +49 -110
- data/benchmarks/model.rb +36 -0
- data/examples/example_diagnosis.rb +35 -73
- data/examples/example_hello.rb +20 -22
- data/examples/example_politician.rb +22 -11
- data/examples/example_ticket.rb +40 -85
- data/examples/fibonacci_example1.rb +13 -2
- data/examples/fibonacci_example2.rb +11 -0
- data/examples/fibonacci_rulebook.rb +58 -111
- data/examples/test_self_reference.rb +51 -9
- data/lib/core/atoms.rb +53 -116
- data/lib/core/engine.rb +96 -96
- data/lib/core/nodes.rb +330 -298
- data/lib/core/patterns.rb +36 -39
- data/lib/core/utils.rb +141 -3
- data/lib/dsl/ferrari.rb +263 -0
- data/lib/dsl/letigre.rb +212 -0
- data/lib/dsl/steel.rb +313 -0
- data/lib/rulebook.rb +82 -265
- data/lib/ruleby.rb +13 -0
- metadata +21 -19
- data/benchmarks/50_joined_rules.rb +0 -78
- data/benchmarks/50_rules.rb +0 -57
- data/benchmarks/5_joined_rules.rb +0 -78
- data/benchmarks/5_rules.rb +0 -57
data/lib/dsl/letigre.rb
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
# This file is part of the Ruleby project (http://ruleby.org)
|
2
|
+
#
|
3
|
+
# This application is free software; you can redistribute it and/or
|
4
|
+
# modify it under the terms of the Ruby license defined in the
|
5
|
+
# LICENSE.txt file.
|
6
|
+
#
|
7
|
+
# Copyright (c) 2007 Joe Kutner and Matt Smith. All rights reserved.
|
8
|
+
#
|
9
|
+
# * Authors: Joe Kutner
|
10
|
+
#
|
11
|
+
|
12
|
+
module Ruleby
|
13
|
+
module LeTigre
|
14
|
+
class RulebookHelper
|
15
|
+
|
16
|
+
def initialize(engine)
|
17
|
+
@engine = engine
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :engine
|
21
|
+
|
22
|
+
def rule(name, *args, &then_block)
|
23
|
+
if args.empty?
|
24
|
+
raise 'No conditions supplied.'
|
25
|
+
end
|
26
|
+
|
27
|
+
options = args[0].kind_of?(Hash) ? args.shift : {}
|
28
|
+
|
29
|
+
pb = PatternParser.new
|
30
|
+
pattern = pb.parse args
|
31
|
+
|
32
|
+
rb = RuleBuilder.new name
|
33
|
+
|
34
|
+
rb.when(pattern)
|
35
|
+
rb.then(&then_block)
|
36
|
+
rb.priority = options[:priority] if options[:priority]
|
37
|
+
|
38
|
+
@engine.assert_rule rb.build_rule
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
class RuleBuilder
|
44
|
+
|
45
|
+
def initialize(name, pattern=nil, action=nil, priority=0)
|
46
|
+
@name = name
|
47
|
+
@pattern = pattern
|
48
|
+
@action = action
|
49
|
+
@priority = priority
|
50
|
+
|
51
|
+
@tags = {}
|
52
|
+
@when_counter = 0
|
53
|
+
end
|
54
|
+
|
55
|
+
def when(pattern)
|
56
|
+
@pattern = pattern
|
57
|
+
end
|
58
|
+
|
59
|
+
def then(&block)
|
60
|
+
@action = Core::Action.new(&block)
|
61
|
+
@action.name = @name
|
62
|
+
@action.priority = @priority
|
63
|
+
end
|
64
|
+
|
65
|
+
def priority
|
66
|
+
return @priority
|
67
|
+
end
|
68
|
+
|
69
|
+
def priority=(p)
|
70
|
+
@priority = p
|
71
|
+
@action.priority = @priority
|
72
|
+
end
|
73
|
+
|
74
|
+
def build_rule
|
75
|
+
Core::Rule.new @name, @pattern, @action, @priority
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class PatternParser
|
80
|
+
@@head_error = 'Invalid type specification.'
|
81
|
+
@@method_error = "No #method in expression: "
|
82
|
+
|
83
|
+
@@base_re = /(For each|\w*\s*exists\??|not\??)(.*)/
|
84
|
+
@@where_re = /(.*) where (.*)/
|
85
|
+
@@head_re = /(\w*)( as :(.*))?/
|
86
|
+
|
87
|
+
@@method_re = /#(\w*|\d*\_*)*\??/
|
88
|
+
@@bind_re = /#:(\w*|\d*\_*)*/
|
89
|
+
@@and_re = /#&&/
|
90
|
+
@@tag_re = /(.*) as :(.*)/
|
91
|
+
|
92
|
+
|
93
|
+
def parse(lhs_strs)
|
94
|
+
pattern = nil
|
95
|
+
lhs_strs.each do |lhs|
|
96
|
+
# match the quantifier
|
97
|
+
if lhs =~ @@base_re
|
98
|
+
base = $1
|
99
|
+
tail = $2
|
100
|
+
else
|
101
|
+
base = 'For each'
|
102
|
+
tail = lhs
|
103
|
+
end
|
104
|
+
|
105
|
+
raise 'The \'exists\' quantifier is not yet supported.' if base =~ /exists/
|
106
|
+
|
107
|
+
# check if there is a where clause
|
108
|
+
if tail =~ @@where_re
|
109
|
+
head = $1.strip
|
110
|
+
tail = $2
|
111
|
+
else
|
112
|
+
head = tail.strip
|
113
|
+
tail = nil
|
114
|
+
end
|
115
|
+
|
116
|
+
# match the class type and tag
|
117
|
+
raise @@head_error unless head =~ @@head_re
|
118
|
+
clazz = eval $1
|
119
|
+
tag = $3 ? $3.to_sym : GeneratedTag.new
|
120
|
+
|
121
|
+
atoms = []
|
122
|
+
atom_strs = tail ? tail.split(@@and_re) : []
|
123
|
+
atom_strs.each do |a|
|
124
|
+
# BUG we also need to pass in the head_tag with atoms!
|
125
|
+
atoms.push parse_atom(a, clazz, atoms)
|
126
|
+
end
|
127
|
+
|
128
|
+
p = (base == 'not?' || base == 'not') ?
|
129
|
+
Core::NotPattern.new(tag, clazz, atoms) :
|
130
|
+
Core::ObjectPattern.new(tag, clazz, atoms)
|
131
|
+
pattern = pattern ? Core::AndPattern.new(pattern, p) : p
|
132
|
+
end
|
133
|
+
return pattern
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
def parse_atom(str, clazz, atoms)
|
138
|
+
expression, tag = nil, nil
|
139
|
+
if str =~ @@tag_re
|
140
|
+
expression, tag = $1, $2.strip.to_sym
|
141
|
+
else
|
142
|
+
expression, tag = str, GeneratedTag.new
|
143
|
+
end
|
144
|
+
|
145
|
+
bindings = []
|
146
|
+
uniq_binds = []
|
147
|
+
expression.scan(@@bind_re).each do |b|
|
148
|
+
# HACK how can we create a truely unique variable name?
|
149
|
+
uniq_bind = "ruleby_unique_variable_name_#{b[0]}"
|
150
|
+
uniq_binds.push uniq_bind
|
151
|
+
expression.sub!(/#:#{b[0]}/, uniq_bind)
|
152
|
+
bindings.push b[0].strip.to_sym
|
153
|
+
end
|
154
|
+
|
155
|
+
raise @@method_error + expression unless expression =~ @@method_re
|
156
|
+
method = $1
|
157
|
+
expression.gsub!(/##{method}/, method)
|
158
|
+
expression = "true" if expression.strip == method
|
159
|
+
|
160
|
+
proc = "lambda {|#{method}"
|
161
|
+
|
162
|
+
uniq_binds.each do |b|
|
163
|
+
# TODO make sure 'b' is not equal to 'method' or other b's
|
164
|
+
proc += ",#{b}"
|
165
|
+
end
|
166
|
+
|
167
|
+
proc += "| #{expression} }"
|
168
|
+
|
169
|
+
block = eval proc
|
170
|
+
|
171
|
+
if bindings.empty?
|
172
|
+
return Core::PropertyAtom.new(tag, method, clazz, &block)
|
173
|
+
elsif references_self?(bindings, atoms)
|
174
|
+
bound_methods = resolve_bindings(bindings, atoms)
|
175
|
+
return Core::SelfReferenceAtom.new(tag, method, bound_methods, clazz, &block)
|
176
|
+
else
|
177
|
+
return Core::ReferenceAtom.new(tag, method, bindings, clazz, &block)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def references_self?(bindings, atoms)
|
182
|
+
ref_self = 0
|
183
|
+
bindings.each do |b|
|
184
|
+
atoms.each do |a|
|
185
|
+
if (a.tag == b)
|
186
|
+
ref_self += 1
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
if ref_self > 0 and ref_self != bindings.size
|
192
|
+
raise 'Binding to self and another pattern in the same expression is not yet supported.'
|
193
|
+
end
|
194
|
+
|
195
|
+
return ref_self > 0
|
196
|
+
end
|
197
|
+
|
198
|
+
def resolve_bindings(bindings, atoms)
|
199
|
+
bound_methods = []
|
200
|
+
bindings.each do |b|
|
201
|
+
atoms.each do |a|
|
202
|
+
if a.tag == b
|
203
|
+
bound_methods.push a.name
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
return bound_methods
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
data/lib/dsl/steel.rb
ADDED
@@ -0,0 +1,313 @@
|
|
1
|
+
# This file is part of the Ruleby project (http://ruleby.org)
|
2
|
+
#
|
3
|
+
# This application is free software; you can redistribute it and/or
|
4
|
+
# modify it under the terms of the Ruby license defined in the
|
5
|
+
# LICENSE.txt file.
|
6
|
+
#
|
7
|
+
# Copyright (c) 2007 Joe Kutner and Matt Smith. All rights reserved.
|
8
|
+
#
|
9
|
+
# * Authors: Matt Smith
|
10
|
+
#
|
11
|
+
|
12
|
+
module Ruleby
|
13
|
+
module Steel
|
14
|
+
class RulebookHelper
|
15
|
+
|
16
|
+
include Ruleby
|
17
|
+
def initialize(engine)
|
18
|
+
@engine = engine
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :engine
|
22
|
+
|
23
|
+
def rule(name, &block)
|
24
|
+
r = Steel::RuleBuilder.new name
|
25
|
+
yield r if block_given?
|
26
|
+
@engine.assert_rule r.build_rule
|
27
|
+
r
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
class RuleBuilder
|
33
|
+
|
34
|
+
def initialize(name, pattern=nil, action=nil, priority=0)
|
35
|
+
@name = name
|
36
|
+
@pattern = pattern
|
37
|
+
@action = action
|
38
|
+
@priority = priority
|
39
|
+
end
|
40
|
+
|
41
|
+
def when(&block)
|
42
|
+
wb = WhenBuilder.new
|
43
|
+
yield wb
|
44
|
+
@pattern = wb.pattern
|
45
|
+
end
|
46
|
+
|
47
|
+
def then(&block)
|
48
|
+
@action = Core::Action.new(&block)
|
49
|
+
@action.name = @name
|
50
|
+
@action.priority = @priority
|
51
|
+
end
|
52
|
+
|
53
|
+
def when=(pattern)
|
54
|
+
@pattern = pattern
|
55
|
+
end
|
56
|
+
|
57
|
+
def then=(action)
|
58
|
+
@action = action
|
59
|
+
@action.name = @name
|
60
|
+
@action.priority = @priority
|
61
|
+
end
|
62
|
+
|
63
|
+
def priority
|
64
|
+
return @priority
|
65
|
+
end
|
66
|
+
|
67
|
+
def priority=(p)
|
68
|
+
@priority = p
|
69
|
+
@action.priority = @priority
|
70
|
+
end
|
71
|
+
|
72
|
+
def build_rule
|
73
|
+
r = Ruleby::Core::Rule.new @name, @pattern, @action, @priority
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class WhenBuilder #< RulebookHelper
|
78
|
+
def initialize()
|
79
|
+
@pattern_hash = Hash.new
|
80
|
+
@pattern_keys = []
|
81
|
+
end
|
82
|
+
|
83
|
+
def method_missing(method_id, *args, &block)
|
84
|
+
method = method_id.to_sym
|
85
|
+
wi = nil
|
86
|
+
if @pattern_hash.key? method
|
87
|
+
wi = @pattern_hash[method]
|
88
|
+
elsif :not == method
|
89
|
+
@pattern_keys.push method
|
90
|
+
return self
|
91
|
+
else
|
92
|
+
wi = WhenInternal.new method, args[0]
|
93
|
+
@pattern_hash[method] = wi
|
94
|
+
@pattern_keys.push method
|
95
|
+
end
|
96
|
+
return wi
|
97
|
+
end
|
98
|
+
|
99
|
+
def pattern
|
100
|
+
operands = []
|
101
|
+
nt = false
|
102
|
+
@pattern_keys.each do |key|
|
103
|
+
if :not != key
|
104
|
+
wi = @pattern_hash[key]
|
105
|
+
tag = wi.tag
|
106
|
+
type = wi.type
|
107
|
+
atoms = wi.to_atoms
|
108
|
+
p = nil
|
109
|
+
if nt
|
110
|
+
p = Ruleby::Core::NotPattern.new(tag, type, atoms)
|
111
|
+
nt = false
|
112
|
+
else
|
113
|
+
p = Ruleby::Core::ObjectPattern.new(tag, type, atoms)
|
114
|
+
end
|
115
|
+
operands = operands + [p]
|
116
|
+
else
|
117
|
+
nt = true
|
118
|
+
end
|
119
|
+
end
|
120
|
+
return and_pattern(operands)
|
121
|
+
end
|
122
|
+
|
123
|
+
def and_pattern(operands)
|
124
|
+
# TODO raise exception if referenceAtoms from the right do not
|
125
|
+
# have the values they referenece in the left
|
126
|
+
# TODO raise exception if there are repeated tags?
|
127
|
+
left = nil
|
128
|
+
operands.each do |operand|
|
129
|
+
if left.nil?
|
130
|
+
left = operand
|
131
|
+
else
|
132
|
+
right = operand
|
133
|
+
left = Ruleby::Core::AndPattern.new(left, right)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
left
|
137
|
+
end
|
138
|
+
|
139
|
+
def or_pattern(operands)
|
140
|
+
# TODO raise exception if referenceAtoms from the right do not
|
141
|
+
# have the values they referenece in the left
|
142
|
+
# TODO raise exception if there are repeated tags?
|
143
|
+
left = nil
|
144
|
+
operands.each do |operand|
|
145
|
+
if left.nil?
|
146
|
+
left = operand
|
147
|
+
else
|
148
|
+
right = operand
|
149
|
+
left = Ruleby::Core::OrPattern.new(left, right)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
left
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class WhenInternal
|
157
|
+
public_instance_methods.each do |m|
|
158
|
+
a = [:method_missing, :new, :public_instance_methods, :__send__, :__id__]
|
159
|
+
undef_method m.to_sym unless a.include? m.to_sym
|
160
|
+
end
|
161
|
+
|
162
|
+
attr_reader :tag, :type
|
163
|
+
def initialize(tag, type)
|
164
|
+
@tag = tag
|
165
|
+
@type = type
|
166
|
+
@builder = WhenPropertyBuilder.new self
|
167
|
+
end
|
168
|
+
|
169
|
+
def to_atoms
|
170
|
+
atoms = []
|
171
|
+
tags = {@tag => :class}
|
172
|
+
@builder.property_hash.each_value do |wp|
|
173
|
+
tags[wp.tag] = wp.name if wp.tag
|
174
|
+
end
|
175
|
+
@builder.property_keys.each do |key|
|
176
|
+
wp = @builder.property_hash[key]
|
177
|
+
atoms = atoms + [wp.to_atom(tags)]
|
178
|
+
end
|
179
|
+
return atoms
|
180
|
+
end
|
181
|
+
|
182
|
+
def &
|
183
|
+
return self
|
184
|
+
end
|
185
|
+
|
186
|
+
def method_missing(method_id, *args, &block)
|
187
|
+
m = method_id.to_s
|
188
|
+
suffix = m.to_s[-1..-1]
|
189
|
+
if suffix == '='
|
190
|
+
new_m = m[0,m.size-1]
|
191
|
+
if args[0].class == Array && args[0].size > 1 && args[0][1] == :%
|
192
|
+
wp = @builder.create new_m do |x,y| x == y end
|
193
|
+
wp.references args[0][0]
|
194
|
+
return wp
|
195
|
+
else
|
196
|
+
wp = @builder.create new_m do |x| x == args[0] end
|
197
|
+
return wp
|
198
|
+
end
|
199
|
+
else
|
200
|
+
wp = @builder.create(m, &block)
|
201
|
+
if args.size > 0 && args[0]
|
202
|
+
if block_given?
|
203
|
+
wp.references args[0]
|
204
|
+
else
|
205
|
+
wp.tag = args[0]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
return wp
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
class WhenPropertyBuilder
|
214
|
+
attr_reader:property_hash
|
215
|
+
attr_reader:property_keys
|
216
|
+
|
217
|
+
def initialize(parent)
|
218
|
+
@parent = parent
|
219
|
+
@property_hash = Hash.new
|
220
|
+
@property_keys = []
|
221
|
+
end
|
222
|
+
|
223
|
+
def create(method_id,&block)
|
224
|
+
method = method_id.to_sym
|
225
|
+
wp = nil
|
226
|
+
if @property_hash.key? method
|
227
|
+
wp = @property_hash[method]
|
228
|
+
else
|
229
|
+
wp = WhenProperty.new @parent, method do |p| true end
|
230
|
+
@property_hash[method] = wp
|
231
|
+
@property_keys.push method
|
232
|
+
end
|
233
|
+
if block_given?
|
234
|
+
wp.block = block
|
235
|
+
end
|
236
|
+
return wp
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
class WhenProperty
|
241
|
+
|
242
|
+
def initialize(parent,name, &block)
|
243
|
+
@tag = nil
|
244
|
+
@name = name
|
245
|
+
@references = nil
|
246
|
+
@block = block
|
247
|
+
@parent = parent
|
248
|
+
end
|
249
|
+
attr:tag,true
|
250
|
+
attr:type,true
|
251
|
+
attr:value,true
|
252
|
+
attr_reader:name
|
253
|
+
attr_accessor:block
|
254
|
+
|
255
|
+
def &
|
256
|
+
return @parent
|
257
|
+
end
|
258
|
+
|
259
|
+
def bind(n)
|
260
|
+
@tag = n
|
261
|
+
end
|
262
|
+
|
263
|
+
def not=(value,ref=nil)
|
264
|
+
if ref && ref == :%
|
265
|
+
raise 'Using \'not=\' for references is not yet supported'
|
266
|
+
set_block do |x,y| x != y end
|
267
|
+
references value
|
268
|
+
else
|
269
|
+
set_block do |s| s != value end
|
270
|
+
end
|
271
|
+
|
272
|
+
end
|
273
|
+
def set_block(&block)
|
274
|
+
@block = block
|
275
|
+
end
|
276
|
+
private:set_block
|
277
|
+
|
278
|
+
def references(refs)
|
279
|
+
@references = refs
|
280
|
+
end
|
281
|
+
|
282
|
+
def to_atom(pattern_tags)
|
283
|
+
unless @tag
|
284
|
+
@tag = GeneratedTag.new
|
285
|
+
end
|
286
|
+
if @references
|
287
|
+
@references = [@references] unless @references.kind_of?(Array)
|
288
|
+
i = includes_how_many(@references, pattern_tags.keys)
|
289
|
+
if i == 0
|
290
|
+
return Ruleby::Core::ReferenceAtom.new(@tag, @name, @references, @parent.type, &@block)
|
291
|
+
elsif i == @references.size
|
292
|
+
refs = @references.collect{|r| pattern_tags[r] }
|
293
|
+
return Ruleby::Core::SelfReferenceAtom.new(@tag, @name, refs, @parent.type, &@block)
|
294
|
+
else
|
295
|
+
raise 'Referencing self AND other patterns in the same atom is not yet supported'
|
296
|
+
end
|
297
|
+
else
|
298
|
+
return Ruleby::Core::PropertyAtom.new(@tag, @name, @parent.type, &@block)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
private
|
303
|
+
def includes_how_many(list1, list2)
|
304
|
+
i = 0
|
305
|
+
list2.each do |a|
|
306
|
+
i += 1 if list1.include?(a)
|
307
|
+
end
|
308
|
+
return i
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
end
|
313
|
+
end
|