ruleby 0.5 → 0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -173,7 +173,7 @@ module Ruleby
173
173
  end
174
174
  end
175
175
 
176
- # This is the core class of the library. A new rule engine is create by
176
+ # This is the core class of the library. A new rule engine is created by
177
177
  # instantiating it. Each rule engine has one inference engine, one rule set
178
178
  # and one working memory.
179
179
  class Engine
@@ -513,6 +513,12 @@ module Ruleby
513
513
  out_node.retract_left(fact)
514
514
  end
515
515
  end
516
+
517
+ def retract_resolve(match)
518
+ @out_nodes.each do |o|
519
+ o.retract_resolve(match)
520
+ end
521
+ end
516
522
  end
517
523
 
518
524
  # This class is used to plug nodes into the right input of a two-input
@@ -528,6 +534,12 @@ module Ruleby
528
534
  @out_nodes.each do |out_node|
529
535
  out_node.retract_right(fact)
530
536
  end
537
+ end
538
+
539
+ def retract_resolve(match)
540
+ @out_nodes.each do |o|
541
+ o.retract_resolve(match)
542
+ end
531
543
  end
532
544
  end
533
545
 
@@ -580,6 +592,19 @@ module Ruleby
580
592
 
581
593
  def to_s
582
594
  return "#{self.class}:#{object_id} | #{@left_memory.values} | #{@right_memory}"
595
+ end
596
+
597
+ def retract_resolve(match)
598
+ # in this method we retract an existing match from memory if it resolves
599
+ # with the match given. It would probably be better to check if it
600
+ # resolves with a list of facts. But the system is not set up for
601
+ # that yet.
602
+ @left_memory.each do |fact_id,contexts|
603
+ contexts.delete_if do |left_context|
604
+ resolve(left_context.match, match)
605
+ end
606
+ end
607
+ propagate_retract_resolve(match)
583
608
  end
584
609
 
585
610
  private
@@ -593,7 +618,8 @@ module Ruleby
593
618
  if ref_mr.is_match
594
619
  mr = mr.merge ref_mr
595
620
  else
596
- return MatchResult.new
621
+ mr = MatchResult.new
622
+ break
597
623
  end
598
624
  end
599
625
  return mr
@@ -615,18 +641,6 @@ module Ruleby
615
641
  o.retract_resolve(match)
616
642
  end
617
643
  end
618
-
619
- def retract_resolve(match)
620
- # in this method we retract an existing match from memory if it resolves
621
- # with the match given. It would probably be better to check if it
622
- # resolves with a list of facts. But the system is not set up for
623
- # that yet.
624
- @left_memory.each do |fact_id,contexts|
625
- value.delete_if do |left_context|
626
- resolve(left_context.match, match)
627
- end
628
- end
629
- end
630
644
  end
631
645
 
632
646
  # This node class is used when a rule is looking for a fact that does not
@@ -4,7 +4,7 @@
4
4
  # modify it under the terms of the Ruby license defined in the
5
5
  # LICENSE.txt file.
6
6
  #
7
- # Copyright (c) 2007 Joe Kutner and Matt Smith. All rights reserved.
7
+ # Copyright (c) 2009 Joe Kutner and Matt Smith. All rights reserved.
8
8
  #
9
9
  # * Authors: Joe Kutner
10
10
  #
@@ -20,37 +20,103 @@ module Ruleby
20
20
 
21
21
  def rule(name, *args, &block)
22
22
  options = args[0].kind_of?(Hash) ? args.shift : {}
23
-
24
- r = RuleBuilder.new name
23
+
24
+ parse_containers(args, RulesContainer.new).build(name,options,@engine,&block)
25
+ end
26
+
27
+ private
28
+ def parse_containers(args, container=AndContainer.new)
29
+ or_builders = []
30
+ and_container = AndContainer.new
25
31
  args.each do |arg|
26
32
  if arg.kind_of? Array
27
- r.when(*arg)
33
+ and_container << PatternContainer.new(arg)
34
+ elsif arg.kind_of? AndBuilder
35
+ and_container << parse_containers(arg.conditions)
36
+ elsif arg.kind_of? OrBuilder
37
+ or_builders << arg
28
38
  else
29
- raise 'Invalid condition. All or none must be Arrays.'
39
+ raise 'Invalid condition. Must be an OR, AND or an Array.'
30
40
  end
31
41
  end
32
-
33
- r.then(&block)
34
- r.priority = options[:priority] if options[:priority]
35
-
36
- @engine.assert_rule(r.build_rule)
42
+
43
+ if or_builders.empty?
44
+ container << and_container
45
+ else
46
+ while !or_builders.empty?
47
+ or_builder = or_builders.pop
48
+ parse_containers(or_builder.conditions, OrContainer.new).each do |or_container|
49
+ or_container.each do |or_container_child|
50
+ rule = AndContainer.new
51
+ rule.push or_container_child
52
+
53
+ or_builders.each do |sub_or_builder|
54
+ parse_containers(sub_or_builder.conditions).each do |sub_or_container|
55
+ rule.push *sub_or_container
56
+ end
57
+ end
58
+
59
+ rule.push and_container
60
+ container << rule
61
+ end
62
+ end
63
+ end
64
+ end
65
+ return container
66
+ end
67
+ end
68
+
69
+ class RulesContainer < Array
70
+ def build(name,options,engine,&block)
71
+ self.each do |x|
72
+ r = RuleBuilder.new name
73
+ x.build r
74
+ r.then(&block)
75
+ r.priority = options[:priority] if options[:priority]
76
+ engine.assert_rule(r.build_rule)
77
+ end
78
+ end
79
+ end
80
+
81
+ class AndContainer < Array
82
+ def build(builder)
83
+ self.each do |x|
84
+ x.build builder
85
+ end
86
+ end
87
+ end
88
+
89
+ class OrContainer < Array
90
+ def build(builder)
91
+ # OrContainers are never built, they just contain containers that
92
+ # will be transformed into AndContainers.
93
+ raise 'Invalid Syntax'
37
94
  end
38
-
39
95
  end
40
96
 
97
+ class PatternContainer
98
+ def initialize(condition)
99
+ @condition = condition
100
+ end
101
+
102
+ def build(builder)
103
+ builder.when(*@condition)
104
+ end
105
+ end
106
+
41
107
  class RuleBuilder
42
-
108
+
43
109
  def initialize(name, pattern=nil, action=nil, priority=0)
44
110
  @name = name
45
111
  @pattern = pattern
46
112
  @action = action
47
113
  @priority = priority
48
-
114
+
49
115
  @tags = {}
50
116
  @methods = {}
51
117
  @when_counter = 0
52
118
  end
53
-
119
+
54
120
  def when(*args)
55
121
  clazz = AtomBuilder === args[0] ? nil : args.shift
56
122
  is_not = false
@@ -65,19 +131,19 @@ module Ruleby
65
131
  end
66
132
  clazz = args.empty? ? nil : args.shift
67
133
  end
68
-
134
+
69
135
  if clazz == nil
70
136
  clazz = Object
71
137
  mode = :inherits
72
138
  end
73
-
139
+
74
140
  deftemplate = Core::DefTemplate.new clazz, mode
75
141
  atoms = []
76
142
  @when_counter += 1
77
143
  htag = Symbol === args[0] ? args.shift : GeneratedTag.new
78
144
  head = Core::HeadAtom.new htag, deftemplate
79
145
  @tags[htag] = @when_counter
80
-
146
+
81
147
  args.each do |arg|
82
148
  if arg.kind_of? Hash
83
149
  arg.each do |ab,tag|
@@ -85,20 +151,20 @@ module Ruleby
85
151
  ab.deftemplate = deftemplate
86
152
  @tags[tag] = @when_counter
87
153
  @methods[tag] = ab.name
88
- atoms.push ab.build_atom(@tags, @methods, @when_counter)
154
+ atoms.push *ab.build_atoms(@tags, @methods, @when_counter)
89
155
  end
90
156
  elsif arg.kind_of? AtomBuilder
91
157
  arg.tag = GeneratedTag.new
92
158
  arg.deftemplate = deftemplate
93
159
  @methods[arg.tag] = arg.name
94
- atoms.push arg.build_atom(@tags, @methods, @when_counter)
160
+ atoms.push *arg.build_atoms(@tags, @methods, @when_counter)
95
161
  elsif arg == false
96
162
  raise 'The != operator is not allowed.'
97
163
  else
98
164
  raise "Invalid condition: #{arg}"
99
165
  end
100
166
  end
101
-
167
+
102
168
  if is_not
103
169
  p = mode==:inherits ? Core::NotInheritsPattern.new(head, atoms) :
104
170
  Core::NotPattern.new(head, atoms)
@@ -107,30 +173,28 @@ module Ruleby
107
173
  Core::ObjectPattern.new(head, atoms)
108
174
  end
109
175
  @pattern = @pattern ? Core::AndPattern.new(@pattern, p) : p
110
-
111
- return nil
112
176
  end
113
-
177
+
114
178
  def then(&block)
115
179
  @action = Core::Action.new(&block)
116
180
  @action.name = @name
117
181
  @action.priority = @priority
118
182
  end
119
-
183
+
120
184
  def priority
121
185
  return @priority
122
186
  end
123
-
187
+
124
188
  def priority=(p)
125
189
  @priority = p
126
190
  @action.priority = @priority
127
191
  end
128
-
192
+
129
193
  def build_rule
130
194
  Core::Rule.new @name, @pattern, @action, @priority
131
195
  end
132
196
  end
133
-
197
+
134
198
  class MethodBuilder
135
199
  public_instance_methods.each do |m|
136
200
  a = [:method_missing, :new, :public_instance_methods, :__send__, :__id__]
@@ -183,12 +247,13 @@ module Ruleby
183
247
  class AtomBuilder
184
248
  attr_accessor :tag, :name, :bindings, :deftemplate, :block
185
249
 
186
- def initialize(name)
187
- @name = name
250
+ def initialize(method_id)
251
+ @name = method_id
188
252
  @deftemplate = nil
189
- @tag = nil
253
+ @tag = GeneratedTag.new
190
254
  @bindings = []
191
255
  @block = lambda {|x| true}
256
+ @child_atom_builders = []
192
257
  end
193
258
 
194
259
  def method_missing(method_id, *args, &block)
@@ -227,21 +292,27 @@ module Ruleby
227
292
  create_block value, lambda {|x,y| x =~ y}, lambda {|x| x =~ value}; self
228
293
  end
229
294
 
230
- def build_atom(tags,methods,when_id)
295
+ def build_atoms(tags,methods,when_id)
296
+ atoms = @child_atom_builders.map { |atom_builder|
297
+ tags[atom_builder.tag] = when_id
298
+ methods[atom_builder.tag] = atom_builder.name
299
+ atom_builder.build_atoms(tags,methods,when_id)
300
+ }.flatten || []
301
+
231
302
  if @bindings.empty?
232
303
  if @atom_type == :equals
233
- return Core::EqualsAtom.new(@tag, @name, @deftemplate, @value)
304
+ return atoms << Core::EqualsAtom.new(@tag, @name, @deftemplate, @value)
234
305
  else
235
- return Core::PropertyAtom.new(@tag, @name, @deftemplate, &@block)
306
+ return atoms << Core::PropertyAtom.new(@tag, @name, @deftemplate, &@block)
236
307
  end
237
308
  end
238
309
 
239
310
  if references_self?(tags,when_id)
240
311
  bind_methods = @bindings.collect{ |bb| methods[bb.tag] }
241
- Core::SelfReferenceAtom.new(@tag,@name,bind_methods,@deftemplate,&@block)
312
+ atoms << Core::SelfReferenceAtom.new(@tag,@name,bind_methods,@deftemplate,&@block)
242
313
  else
243
314
  bind_tags = @bindings.collect{ |bb| bb.tag }
244
- Core::ReferenceAtom.new(@tag,@name,bind_tags,@deftemplate,&@block)
315
+ atoms << Core::ReferenceAtom.new(@tag,@name,bind_tags,@deftemplate,&@block)
245
316
  end
246
317
  end
247
318
 
@@ -265,6 +336,10 @@ module Ruleby
265
336
  if value && value.kind_of?(BindingBuilder)
266
337
  @bindings = [value]
267
338
  @block = ref_block
339
+ elsif value && value.kind_of?(AtomBuilder)
340
+ @child_atom_builders << value
341
+ @bindings = [BindingBuilder.new(value.tag)]
342
+ @block = ref_block
268
343
  else
269
344
  @block = basic_block
270
345
  end
@@ -276,5 +351,19 @@ module Ruleby
276
351
  create_block value, lambda {|x,y| x != y}, lambda {|x| x != value}; self
277
352
  end
278
353
  end
354
+
355
+ class OrBuilder
356
+ attr_reader :conditions
357
+ def initialize(conditions)
358
+ @conditions = conditions
359
+ end
360
+ end
361
+
362
+ class AndBuilder
363
+ attr_reader :conditions
364
+ def initialize(conditions)
365
+ @conditions = conditions
366
+ end
367
+ end
279
368
  end
280
369
  end
File without changes
@@ -17,13 +17,12 @@ require 'dsl/steel'
17
17
  module Ruleby
18
18
  class Rulebook
19
19
  include Ruleby
20
- def initialize(engine, session={})
20
+ def initialize(engine, &block)
21
21
  @engine = engine
22
- @session = session
22
+ yield self if block_given?
23
23
  end
24
24
 
25
25
  attr_reader :engine
26
- attr_reader :session
27
26
 
28
27
  def assert(fact)
29
28
  @engine.assert fact
@@ -45,7 +44,7 @@ module Ruleby
45
44
  r.rule name, &block
46
45
  else
47
46
  i = args[0].kind_of?(Hash) ? 1 : 0
48
- if args[i].kind_of? Array
47
+ if [Array, Ruleby::Ferrari::OrBuilder, Ruleby::Ferrari::AndBuilder].include? args[i].class
49
48
  # use ferrari DSL
50
49
  r = Ferrari::RulebookHelper.new @engine
51
50
  r.rule name, *args, &block
@@ -72,7 +71,7 @@ module Ruleby
72
71
  end
73
72
 
74
73
  def binding(variable_name)
75
- b
74
+ b variable_name
76
75
  end
77
76
 
78
77
  def c(&block)
@@ -83,6 +82,14 @@ module Ruleby
83
82
  return lambda(&block)
84
83
  end
85
84
 
85
+ def OR(*args)
86
+ Ruleby::Ferrari::OrBuilder.new args
87
+ end
88
+
89
+ def AND(*args)
90
+ Ruleby::Ferrari::AndBuilder.new args
91
+ end
92
+
86
93
  def __eval__(x)
87
94
  eval(x)
88
95
  end
@@ -9,10 +9,11 @@
9
9
  # * Authors: John Mettraux
10
10
  #
11
11
  require 'common'
12
- require 'test_duck_type'
13
- require 'test_self_reference'
14
- require 'test_re'
15
- require 'gets.rb'
16
- require 'test_assert_facts'
12
+ require 'duck_type'
13
+ require 'self_reference'
14
+ require 'regex'
15
+ require 'gets'
16
+ require 'assert_facts'
17
17
  require 'not_patterns'
18
-
18
+ require 'or_patterns'
19
+ require 'join_nodes'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruleby
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.5"
4
+ version: "0.6"
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe Kutner
@@ -10,12 +10,17 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2009-06-16 00:00:00 -05:00
13
+ date: 2009-11-23 00:00:00 -06:00
14
14
  default_executable:
15
15
  dependencies: []
16
16
 
17
- description: Ruleby is a rule engine written in the Ruby language. It is a system for executing a set of IF-THEN statements known as production rules. These rules are matched to objects using the forward chaining Rete algorithm. Ruleby provides an internal Domain Specific Language (DSL) for building the productions that make up a Ruleby program. Release Notes for Version 0.5 * reset the TerminalNode.counter when initializing the RootNode. Thanks to Shashank for the patch. * added InitialFact so that NotPatterns can be put at the front of a rule [#1 status:resolved] * Improved rule assertion so that rules can be added after facts have been asserted [#9 state:resolved] * Added retrieve method to engine, and a unit-test for it
18
- email: matt@ruleby.org
17
+ description: |
18
+ Ruleby is a rule engine written in the Ruby language. It is a system for executing a set
19
+ of IF-THEN statements known as production rules. These rules are matched to objects using
20
+ the forward chaining Rete algorithm. Ruleby provides an internal Domain Specific Language
21
+ (DSL) for building the productions that make up a Ruleby program.
22
+
23
+ email: jpkutner@gmail.com
19
24
  executables: []
20
25
 
21
26
  extensions: []
@@ -23,24 +28,20 @@ extensions: []
23
28
  extra_rdoc_files: []
24
29
 
25
30
  files:
26
- - lib/core
27
31
  - lib/core/atoms.rb
28
32
  - lib/core/engine.rb
29
33
  - lib/core/nodes.rb
30
34
  - lib/core/patterns.rb
31
35
  - lib/core/utils.rb
32
- - lib/dsl
33
36
  - lib/dsl/ferrari.rb
34
37
  - lib/dsl/letigre.rb
35
38
  - lib/dsl/steel.rb
36
- - lib/dsl/treetop
37
- - lib/dsl/treetop/treetop_helper.rb
38
- - lib/dsl/treetop/tt_dsl.treetop
39
- - lib/dsl/yaml_dsl.rb
40
39
  - lib/rulebook.rb
41
40
  - lib/ruleby.rb
42
41
  has_rdoc: true
43
42
  homepage: http://ruleby.org
43
+ licenses: []
44
+
44
45
  post_install_message:
45
46
  rdoc_options: []
46
47
 
@@ -61,9 +62,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
62
  requirements: []
62
63
 
63
64
  rubyforge_project: ruleby
64
- rubygems_version: 1.0.1
65
+ rubygems_version: 1.3.5
65
66
  signing_key:
66
- specification_version: 2
67
+ specification_version: 3
67
68
  summary: Rete based Ruby Rule Engine
68
69
  test_files:
69
70
  - tests/test.rb
@@ -1,23 +0,0 @@
1
-
2
- module Ruleby
3
- class TreetopHelper
4
-
5
- def self.rule(name, *args, &block)
6
- options = args[0].kind_of?(Hash) ? args.shift : {}
7
-
8
- r = Ferrari::RuleBuilder.new name
9
- args.each do |arg|
10
- if arg.kind_of? Array
11
- r.when(*arg)
12
- else
13
- raise 'Invalid condition. All or none must be Arrays.'
14
- end
15
- end
16
-
17
- r.then(&block)
18
- r.priority = options[:priority] if options[:priority]
19
-
20
- return r.build_rule
21
- end
22
- end
23
- end
@@ -1,105 +0,0 @@
1
- grammar Ruleby
2
- rule root
3
- defrule* {
4
- def get_rules
5
- rs = []
6
- elements.each do |e|
7
- rs << e.get_rule()
8
- end
9
- return rs
10
- end
11
- }
12
- end
13
-
14
- rule defrule
15
- 'rule' space name space foreach space action {
16
- def get_rule()
17
- action_text = action.text_value.strip
18
- action_text.gsub!('do', '')
19
- action_text.gsub!('end', '')
20
- return foreach.get_rule(name.text_value.strip, action_text.strip)
21
- end
22
- }
23
- end
24
-
25
- rule name
26
- [a-zA-Z] [a-zA-Z0-9]*
27
- end
28
-
29
- rule foreach
30
- 'foreach' space head space symbol {
31
- def get_rule(name, action_text)
32
- class_name = head.text_value.strip
33
- clazz = eval(class_name)
34
- tag = symbol.text_value.strip
35
- tag = tag[1, tag.size - 1]
36
- return Ruleby::TreetopHelper.rule(name, [clazz, tag.to_sym], &action_text)
37
- end
38
- }
39
- end
40
-
41
-
42
- rule action
43
- 'do' space (!'end' .)* 'end' space?
44
- end
45
-
46
-
47
-
48
- rule head
49
- [A-Z] [a-zA-Z]*
50
- end
51
- rule symbol
52
- ':' [a-zA-Z]+
53
- end
54
- rule where
55
- 'where'
56
- end
57
- rule method
58
- symbol '.' [a-z] [a-zA-Z0-9]*
59
- end
60
- rule string
61
- '\'' [a-zA-Z0-9]* '\''
62
- end
63
- rule value
64
- method / '-'* [0-9]+ / symbol / string
65
- end
66
- rule expression
67
- value space (equal space value) / (not_equal space value) / (symbol)
68
- end
69
-
70
- rule clause
71
- expression more:(and_sign clause)* {
72
- def populate
73
- more.elements.each do |e|
74
- e.clause.populate
75
- end
76
- end
77
- }
78
- end
79
- rule and_sign
80
- 'AND'
81
- end
82
- rule not_equal
83
- '!='
84
- end
85
- rule equal
86
- '=='
87
- end
88
-
89
- rule space
90
- white+
91
- end
92
-
93
-
94
- rule white
95
- blank / eol
96
- end
97
-
98
- rule blank
99
- [ \t]
100
- end
101
-
102
- rule eol
103
- ("\r" "\n"?) / "\n"
104
- end
105
- end
@@ -1,23 +0,0 @@
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