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.
@@ -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
@@ -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