ruleby 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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