├── .gitignore ├── spec ├── spec.opts ├── coercion_spec.rb ├── spec_helper.rb ├── hello_spec.rb ├── ferrari_spec.rb ├── node_sharing_spec.rb ├── property_spec.rb ├── errors_spec.rb ├── function_spec.rb ├── and_or_spec.rb └── collect_spec.rb ├── tasks ├── test.rake ├── rspec.rake └── documentation.rake ├── tests ├── common.rb ├── test.rb ├── regex.rb ├── gets.rb ├── self_reference.rb ├── join_nodes.rb ├── nil.rb ├── duck_type.rb ├── not_patterns.rb ├── assert_facts.rb └── or_patterns.rb ├── lib ├── ruleby.rb ├── rule_helper.rb ├── rulebook.rb ├── core │ ├── patterns.rb │ ├── atoms.rb │ ├── engine.rb │ └── utils.rb └── dsl │ └── ferrari.rb ├── README.markdown ├── ruleby.gemspec ├── benchmarks ├── miss_manners │ ├── miss_manners.rb │ ├── rules.rb │ ├── data.rb │ └── model.rb ├── model.rb ├── basic_rules.rb └── joined_rules.rb ├── examples ├── fibonacci_example2.rb ├── fibonacci_example1.rb ├── hello.rb ├── fibonacci_example3.rb ├── fibonacci_rulebook.rb ├── ticket.rb ├── diagnosis.rb └── wordgame.rb ├── LICENSE.txt └── GPL.txt /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | *.gem 3 | ruleby.tmproj 4 | nbproject 5 | .idea 6 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --colour 2 | --loadby random 3 | --format profile 4 | --backtrace -------------------------------------------------------------------------------- /spec/coercion_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ruleby::Core::Engine do 4 | 5 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'ruleby' 2 | #require 'rspec' 3 | 4 | class Success 5 | attr :status, true 6 | def initialize(status=nil) 7 | @status = status 8 | end 9 | end -------------------------------------------------------------------------------- /tasks/test.rake: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # TESTING 4 | 5 | Rake::TestTask.new(:test) do |t| 6 | t.libs << "tests" 7 | t.test_files = FileList['tests/test.rb'] 8 | t.verbose = true 9 | end 10 | -------------------------------------------------------------------------------- /tasks/rspec.rake: -------------------------------------------------------------------------------- 1 | begin 2 | require 'spec' 3 | rescue LoadError 4 | require 'rubygems' 5 | require 'spec' 6 | end 7 | begin 8 | require 'spec/rake/spectask' 9 | rescue LoadError 10 | puts <<-EOS 11 | To use rspec for testing you must install rspec gem: 12 | gem install rspec 13 | EOS 14 | exit(0) 15 | end 16 | 17 | desc "Run the specs under spec/models" 18 | Spec::Rake::SpecTask.new do |t| 19 | t.spec_opts = ['--options', "spec/spec.opts"] 20 | t.spec_files = FileList['spec/**/*_spec.rb'] 21 | end -------------------------------------------------------------------------------- /tests/common.rb: -------------------------------------------------------------------------------- 1 | 2 | class Context 3 | 4 | def initialize 5 | @counts = {} 6 | @counts.default = 0 7 | end 8 | 9 | def inc(key) 10 | @counts[key] += 1 11 | end 12 | 13 | def set(key,value) 14 | @counts[key] = value 15 | end 16 | 17 | def get(key) 18 | @counts[key] 19 | end 20 | end 21 | 22 | class Message 23 | def initialize(status,message) 24 | @status = status 25 | @message = message 26 | end 27 | attr :status, true 28 | attr :message, true 29 | end -------------------------------------------------------------------------------- /tests/test.rb: -------------------------------------------------------------------------------- 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) 2008 Joe Kutner and Matt Smith. All rights reserved. 8 | # 9 | # * Authors: John Mettraux 10 | # 11 | require 'common' 12 | require 'duck_type' 13 | require 'self_reference' 14 | require 'regex' 15 | require 'gets' 16 | require 'assert_facts' 17 | require 'not_patterns' 18 | require 'or_patterns' 19 | require 'join_nodes' 20 | require 'nil' 21 | -------------------------------------------------------------------------------- /lib/ruleby.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | require 'core/engine' 13 | require 'rulebook' 14 | 15 | module Ruleby 16 | #helper classes for using ruleby go here 17 | def engine(name, &block) 18 | e = Core::Engine.new 19 | yield e if block_given? 20 | return e 21 | end 22 | end -------------------------------------------------------------------------------- /spec/hello_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class HelloFact 4 | attr :value, true 5 | def initialize(v=nil); @value = v; end 6 | end 7 | 8 | include Ruleby 9 | 10 | class HelloRulebook < Rulebook 11 | def rules 12 | rule [HelloFact, :h] do 13 | assert Success.new 14 | end 15 | end 16 | end 17 | 18 | describe Ruleby::Core::Engine do 19 | subject do 20 | engine :engine do |e| 21 | HelloRulebook.new(e).rules 22 | end 23 | end 24 | 25 | before do 26 | subject.assert HelloFact.new 27 | subject.match 28 | end 29 | 30 | it "should have matched" do 31 | subject.errors.should == [] 32 | subject.retrieve(Success).size.should == 1 33 | end 34 | end -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Ruleby :: The Rule Engine for Ruby 2 | ================================== 3 | 4 | Description 5 | ----------- 6 | Ruleby is a rule engine written in the Ruby language. It is a system for executing a set 7 | of IF-THEN statements known as production rules. These rules are matched to objects using 8 | the forward chaining Rete algorithm. Ruleby provides an internal Domain Specific Language 9 | (DSL) for building the productions that make up a Ruleby program. 10 | 11 | Version 12 | ------- 13 | 0.9.b7 14 | 15 | Release Notes 16 | ------------- 17 | 18 | + Major improvements to AND and OR in ferrari DSL - especially when nesting them. 19 | 20 | Mailing List 21 | ------------ 22 | ruleby@googlegroups.com 23 | -------------------------------------------------------------------------------- /spec/ferrari_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class A 4 | 5 | end 6 | 7 | include Ruleby 8 | 9 | class FerrariRulebook < Rulebook 10 | def rules 11 | rule [A] do |v| 12 | assert Success.new 13 | end 14 | end 15 | end 16 | 17 | describe Ruleby::Core::Engine do 18 | 19 | subject do 20 | engine :engine do |e| 21 | FerrariRulebook.new(e).rules 22 | end 23 | end 24 | 25 | describe "simple case" do 26 | context "with one A" do 27 | before do 28 | subject.assert A.new 29 | subject.match 30 | end 31 | 32 | it "should retrieve Success" do 33 | s = subject.retrieve Success 34 | s.should_not be_nil 35 | s.size.should == 1 36 | end 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /ruleby.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = %q{ruleby} 3 | s.version = "0.9.b7" 4 | 5 | s.authors = [%q{Joe Kutner}, %q{Matt Smith}] 6 | s.description = %q{Ruleby is a rule engine written in the Ruby language. It is a system for executing a set 7 | of IF-THEN statements known as production rules. These rules are matched to objects using 8 | the forward chaining Rete algorithm. Ruleby provides an internal Domain Specific Language 9 | (DSL) for building the productions that make up a Ruleby program. 10 | } 11 | s.email = %q{jpkutner@gmail.com} 12 | s.homepage = %q{http://ruleby.org} 13 | s.files = `git ls-files`.split("\n") 14 | s.require_paths = [%q{lib}] 15 | s.rubyforge_project = %q{ruleby} 16 | s.summary = %q{Rete based Ruby Rule Engine} 17 | s.license = %q{Ruby} 18 | end 19 | -------------------------------------------------------------------------------- /tasks/documentation.rake: -------------------------------------------------------------------------------- 1 | # 2 | # DOCUMENTATION 3 | 4 | #ALLISON=`allison --path` 5 | #ALLISON="/Library/Ruby/Gems/1.8/gems/allison-2.0.3/lib/allison.rb" 6 | 7 | Rake::RDocTask.new do |rd| 8 | 9 | #rd.main = "README.txt" 10 | #rd.rdoc_dir = "html/rufus-verbs" 11 | 12 | rd.rdoc_files.include( 13 | "LICENSE.txt", 14 | "lib/**/*.rb") 15 | 16 | rd.title = "ruleby rdoc" 17 | 18 | rd.options << '-N' # line numbers 19 | rd.options << '-S' # inline source 20 | 21 | #rd.template = ALLISON if File.exist?(ALLISON) 22 | end 23 | 24 | 25 | # 26 | # WEBSITE 27 | 28 | #task :upload_website => [ :clean, :rdoc ] do 29 | # account = "whoever@rubyforge.org" 30 | # webdir = "/var/www/gforge-projects/ruleby" 31 | # sh "rsync -azv -e ssh html/source #{account}:#{webdir}/" 32 | #end -------------------------------------------------------------------------------- /benchmarks/miss_manners/miss_manners.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | $LOAD_PATH << File.join(File.dirname(__FILE__), '../../lib/') 13 | require 'ruleby' 14 | require 'model' 15 | require 'data' 16 | require 'rules' 17 | 18 | include Ruleby 19 | include MissManners 20 | 21 | t1 = Time.new 22 | engine :e do |e| 23 | MannersRulebook.new(e).rules 24 | MannersData.new.guests16.each do |g| 25 | e.assert g 26 | end 27 | e.assert Context.new(:start) 28 | e.assert Count.new(1) 29 | e.match 30 | end 31 | t2 = Time.new 32 | diff = t2.to_f - t1.to_f 33 | puts diff.to_s -------------------------------------------------------------------------------- /benchmarks/model.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | class Account 13 | def initialize(status, title, account_id) 14 | @status = status 15 | @title = title 16 | @account_id = account_id 17 | end 18 | 19 | attr :status, true 20 | attr :title, true 21 | attr :account_id, true 22 | end 23 | 24 | class Address 25 | def initialize(addr_id, city, state, zip) 26 | @addr_id = addr_id 27 | @city = city 28 | @state = state 29 | @zip = zip 30 | end 31 | 32 | attr :addr_id, true 33 | attr :city, true 34 | attr :state, true 35 | attr :zip, true 36 | end -------------------------------------------------------------------------------- /examples/fibonacci_example2.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/') 13 | require 'ruleby' 14 | require 'fibonacci_rulebook' 15 | class Fibonacci 16 | def initialize(sequence,value=-1) 17 | @sequence = sequence 18 | @value = value 19 | end 20 | 21 | attr_reader :sequence 22 | attr :value, true 23 | 24 | def to_s 25 | return super + "::sequence=" + @sequence.to_s + ",value=" + @value.to_s 26 | end 27 | end 28 | 29 | include Ruleby 30 | 31 | # FACTS 32 | fib1 = Fibonacci.new(1,1) 33 | fib2 = Fibonacci.new(2,1) 34 | 35 | engine :engine do |e| 36 | FibonacciRulebook2.new(e).rules 37 | e.assert fib1 38 | e.assert fib2 39 | e.match 40 | end -------------------------------------------------------------------------------- /spec/node_sharing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class NodeShareFact 4 | attr_reader :times 5 | 6 | def initialize(v=nil); @value = v; @times = 0; end 7 | 8 | def value 9 | @times += 1 10 | @value 11 | end 12 | end 13 | 14 | include Ruleby 15 | 16 | class NodeShareRulebook < Rulebook 17 | def rules 18 | rule [NodeShareFact, :n, m.value == 5] do 19 | assert Success.new 20 | end 21 | 22 | rule [NodeShareFact, :n, m.value == 5] do 23 | assert Success.new 24 | end 25 | 26 | rule [NodeShareFact, :n, m.value == 6] do 27 | assert Success.new 28 | end 29 | end 30 | end 31 | 32 | describe Ruleby::Core::Engine do 33 | 34 | describe "node sharing" do 35 | subject do 36 | engine :engine do |e| 37 | NodeShareRulebook.new(e).rules 38 | end 39 | end 40 | 41 | before do 42 | @f = NodeShareFact.new(5) 43 | subject.assert @f 44 | subject.match 45 | end 46 | 47 | it "should have matched" do 48 | @f.times.should == 1 49 | subject.errors.should == [] 50 | subject.retrieve(Success).size.should == 2 51 | end 52 | end 53 | end -------------------------------------------------------------------------------- /tests/regex.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | require 'test/unit' 13 | require 'ruleby' 14 | 15 | include Ruleby 16 | 17 | MY_RE = /slot_(\d+)/ 18 | 19 | module RE 20 | 21 | class RERulebook < Rulebook 22 | def rules 23 | 24 | rule [Message, :m, m.message =~ MY_RE], [Context, :c] do |v| 25 | v[:c].inc :my_re 26 | end 27 | 28 | end 29 | end 30 | 31 | class Test < Test::Unit::TestCase 32 | 33 | def test_0 34 | ctx = Context.new 35 | 36 | engine :engine do |e| 37 | RERulebook.new(e).rules 38 | e.assert ctx 39 | e.assert Message.new(:HELLO, 'slot_1') 40 | e.assert Message.new(:HELLO, 'slot_x') 41 | e.match 42 | end 43 | 44 | assert_equal 1, ctx.get(:my_re) 45 | end 46 | end 47 | end -------------------------------------------------------------------------------- /tests/gets.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | require 'test/unit' 13 | require 'ruleby' 14 | 15 | include Ruleby 16 | 17 | TEST_STR = String.new("it worked") 18 | 19 | module Get 20 | 21 | class GetRulebook < Rulebook 22 | def rules 23 | 24 | rule [Message, :m, m.status == :HELLO] do |v| 25 | @engine.assert TEST_STR 26 | end 27 | 28 | end 29 | end 30 | 31 | class Test < Test::Unit::TestCase 32 | 33 | def test_0 34 | 35 | engine :engine do |e| 36 | GetRulebook.new(e).rules 37 | e.assert Message.new(:HELLO, 'test') 38 | e.match 39 | 40 | strs = e.retrieve(String) 41 | 42 | assert_equal 1, strs.size 43 | 44 | assert_equal TEST_STR, strs[0] 45 | end 46 | end 47 | end 48 | end -------------------------------------------------------------------------------- /examples/fibonacci_example1.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/') 13 | require 'ruleby' 14 | require 'fibonacci_rulebook' 15 | class Fibonacci 16 | def initialize(sequence,value=-1) 17 | @sequence = sequence 18 | @value = value 19 | end 20 | 21 | attr_reader :sequence 22 | attr :value, true 23 | 24 | def to_s 25 | return '['+super + " sequence=" + @sequence.to_s + ",value=" + @value.to_s + ']' 26 | end 27 | end 28 | 29 | include Ruleby 30 | 31 | # This example is borrowed from the JBoss-Rule project. 32 | 33 | # FACTS 34 | fib1 = Fibonacci.new(150) 35 | 36 | t1 = Time.new 37 | engine :engine do |e| 38 | FibonacciRulebookFerrari.new(e).rules 39 | e.assert fib1 40 | e.match 41 | end 42 | t2 = Time.new 43 | diff = t2.to_f - t1.to_f 44 | puts diff.to_s -------------------------------------------------------------------------------- /examples/hello.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/') 13 | require 'ruleby' 14 | 15 | include Ruleby 16 | 17 | class Message 18 | def initialize(status,message) 19 | @status = status 20 | @message = message 21 | end 22 | attr :status, true 23 | attr :message, true 24 | end 25 | 26 | class HelloWorldRulebook < Rulebook 27 | def rules 28 | rule [Message, :m, m.status == :HELLO] do |v| 29 | puts v[:m].message 30 | v[:m].message = "Goodbye world" 31 | v[:m].status = :GOODBYE 32 | modify v[:m] 33 | end 34 | 35 | rule [Message, :m, m.status == :GOODBYE] do |v| 36 | puts v[:m].message 37 | end 38 | end 39 | end 40 | 41 | engine :engine do |e| 42 | HelloWorldRulebook.new(e).rules 43 | e.assert Message.new(:HELLO, 'Hello World') 44 | e.match 45 | end -------------------------------------------------------------------------------- /tests/self_reference.rb: -------------------------------------------------------------------------------- 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) 2008 Joe Kutner and Matt Smith. All rights reserved. 8 | # 9 | # * Authors: Joe Kutner, Matt Smith, John Mettraux 10 | # 11 | 12 | require 'test/unit' 13 | 14 | require 'ruleby' 15 | 16 | include Ruleby 17 | 18 | module SelfReference 19 | 20 | class SelfRefRulebook < Rulebook 21 | def rules 22 | rule [Message, :m, {m.message => :x}, m.status == b(:x)], 23 | [Context, :c] do |v| 24 | v[:c].inc :rule2 25 | end 26 | 27 | # This is effectively the same as the rule above 28 | rule [Message, :m, m.status == m.message], 29 | [Context, :c] do |v| 30 | v[:c].inc :rule3 31 | end 32 | 33 | end 34 | end 35 | 36 | 37 | class Test < Test::Unit::TestCase 38 | 39 | def test_0 40 | 41 | engine :engine do |e| 42 | SelfRefRulebook.new(e).rules 43 | ctx = Context.new 44 | e.assert ctx 45 | e.assert Message.new(:HELLO, :HELLO) 46 | e.assert Message.new(:HELLO, :GOODBYE) 47 | e.match 48 | 49 | assert_equal 1, ctx.get(:rule2) 50 | assert_equal 1, ctx.get(:rule3) 51 | end 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /tests/join_nodes.rb: -------------------------------------------------------------------------------- 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) 2009 Joe Kutner and Matt Smith. All rights reserved. 8 | # 9 | # * Authors: Joe Kutner 10 | # 11 | 12 | require 'test/unit' 13 | 14 | require 'ruleby' 15 | 16 | include Ruleby 17 | 18 | module JoinNodes 19 | 20 | class A 21 | 22 | end 23 | 24 | class B 25 | 26 | end 27 | 28 | class JoinNodesRulebook < Rulebook 29 | def rules 30 | rule [A], [B], [Context, :c] do |v| 31 | v[:c].inc :rule1 32 | end 33 | end 34 | end 35 | 36 | class Test < Test::Unit::TestCase 37 | def test_0 38 | engine :engine do |e| 39 | JoinNodesRulebook.new(e).rules 40 | ctx = Context.new 41 | a = A.new 42 | b = B.new 43 | e.assert ctx 44 | e.assert a 45 | e.assert b 46 | e.match 47 | assert_equal 1, ctx.get(:rule1) 48 | e.retract a 49 | e.match 50 | assert_equal 1, ctx.get(:rule1) 51 | e.assert B.new 52 | e.match 53 | assert_equal 1, ctx.get(:rule1) 54 | e.assert A.new 55 | e.match 56 | assert_equal 3, ctx.get(:rule1) 57 | e.retract b 58 | e.match 59 | assert_equal 4, ctx.get(:rule1) 60 | end 61 | end 62 | end 63 | end -------------------------------------------------------------------------------- /spec/property_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class PropFact 4 | attr :value, true 5 | def initialize(v=nil); @value = v; end 6 | end 7 | 8 | class PropCtx 9 | 10 | end 11 | 12 | 13 | include Ruleby 14 | 15 | class PropRulebook < Rulebook 16 | def gt_rules 17 | rule [PropFact, :p, m.value > 0] do 18 | assert Success.new 19 | end 20 | end 21 | 22 | def lte_rules 23 | rule [PropFact, :p, m.value > 42], [PropCtx, :pc] do 24 | # do nothing, just being here helps reproduce a bug 25 | end 26 | rule [PropFact, :p, m.value <= 42], [PropCtx, :pc] do 27 | assert Success.new 28 | end 29 | end 30 | end 31 | 32 | describe Ruleby::Core::Engine do 33 | describe "property gt_rules" do 34 | subject do 35 | engine :engine do |e| 36 | PropRulebook.new(e).gt_rules 37 | end 38 | end 39 | 40 | before do 41 | subject.assert PropFact.new(1) 42 | subject.match 43 | end 44 | 45 | it "should have matched" do 46 | subject.errors.should == [] 47 | subject.retrieve(Success).size.should == 1 48 | end 49 | end 50 | describe "property lte_rules" do 51 | subject do 52 | engine :engine do |e| 53 | PropRulebook.new(e).lte_rules 54 | end 55 | end 56 | 57 | before do 58 | subject.assert PropCtx.new 59 | subject.assert PropFact.new(42) 60 | subject.assert PropFact.new(41) 61 | subject.match 62 | end 63 | 64 | it "should have matched" do 65 | subject.errors.should == [] 66 | subject.retrieve(Success).size.should == 2 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /benchmarks/basic_rules.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/') 13 | require 'ruleby' 14 | require 'model' 15 | 16 | include Ruleby 17 | 18 | class TestRulebook < Rulebook 19 | def rules(n) 20 | (0..n).each do |index| 21 | rule "Rule-#{index}".to_sym, 22 | [Account, 23 | method.status == 'standard', 24 | method.title == 'mr', 25 | method.account_id == "acc#{index}"] do |vars| 26 | # puts "rule #{index} fired" 27 | end 28 | end 29 | end 30 | end 31 | 32 | def run_benchmark(rules, facts) 33 | puts "running benchmark for: #{rules} rules and #{facts} facts" 34 | 35 | t1 = Time.new 36 | engine :engine do |e| 37 | TestRulebook.new(e).rules(rules) 38 | 39 | t2 = Time.new 40 | diff = t2.to_f - t1.to_f 41 | puts 'time to create rule set: ' + diff.to_s 42 | for k in (0..facts) 43 | e.assert Account.new('standard', 'mr', ('acc'+k.to_s)) 44 | end 45 | 46 | t3 = Time.new 47 | diff = t3.to_f - t2.to_f 48 | puts 'time to assert facts: ' + diff.to_s 49 | e.match 50 | 51 | t4 = Time.new 52 | diff = t4.to_f - t3.to_f 53 | puts 'time to run agenda: ' + diff.to_s 54 | end 55 | end 56 | 57 | run_benchmark(5, 500) 58 | puts '-------' 59 | run_benchmark(50, 500) 60 | puts '-------' 61 | run_benchmark(500, 500) 62 | -------------------------------------------------------------------------------- /benchmarks/joined_rules.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/') 13 | require 'ruleby' 14 | require 'model' 15 | 16 | include Ruleby 17 | 18 | class TestRulebook < Rulebook 19 | def rules(n) 20 | (1..n).each do |index| 21 | rule "Rule-#{index}".to_sym, 22 | [Account, :acc, 23 | method.status == 'standard', 24 | {method.account_id => :id} 25 | ], 26 | [Address, :addr, 27 | method.addr_id == b(:id), 28 | method.city == 'Foobar', 29 | method.state == 'FB', 30 | method.zip == '12345'], 31 | &lambda {|vars| } 32 | end 33 | end 34 | end 35 | 36 | def run_benchmark(rules,facts) 37 | puts "running benchmark for: #{rules} rules and #{facts} facts" 38 | 39 | t1 = Time.new 40 | engine :engine do |e| 41 | TestRulebook.new(e).rules(rules) 42 | 43 | t2 = Time.new 44 | diff = t2.to_f - t1.to_f 45 | puts 'time to create rule set: ' + diff.to_s 46 | #e.print 47 | for k in (1..facts) 48 | e.assert Account.new('standard', nil, "acc#{k}") 49 | e.assert Address.new("acc#{k}",'Foobar', 'FB', '12345') 50 | end 51 | #exit(0) 52 | t3 = Time.new 53 | diff = t3.to_f - t2.to_f 54 | puts 'time to assert facts: ' + diff.to_s 55 | 56 | e.match 57 | 58 | t4 = Time.new 59 | diff = t4.to_f - t3.to_f 60 | puts 'time to run agenda: ' + diff.to_s 61 | end 62 | end 63 | 64 | run_benchmark(5, 50) 65 | puts '-------' 66 | run_benchmark(50, 50) 67 | -------------------------------------------------------------------------------- /lib/rule_helper.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | require 'core/engine' 13 | 14 | module Ruleby 15 | module RuleHelper 16 | def rule(*args, &block) 17 | name = nil 18 | unless args.empty? 19 | name = args[0].kind_of?(Symbol) ? args.shift : GeneratedTag.new 20 | end 21 | options = args[0].kind_of?(Hash) ? args.shift : {} 22 | 23 | rules = Ruleby::Ferrari.parse_containers(args, Ruleby::Ferrari::RulesContainer.new).build(name,options,@engine,&block) 24 | rules 25 | end 26 | 27 | def m 28 | Ruleby::Ferrari::MethodBuilder.new 29 | end 30 | 31 | def method 32 | m 33 | end 34 | 35 | def b(variable_name) 36 | Ruleby::Ferrari::BindingBuilder.new(variable_name) 37 | end 38 | 39 | def c(&block) 40 | lambda(&block) 41 | end 42 | 43 | def f(args, block=nil) 44 | if block.nil? 45 | if !args.is_a?(Proc) 46 | raise "You must provide a Proc!" 47 | else 48 | Ruleby::Ferrari::FunctionBuilder.new([], args) 49 | end 50 | else 51 | if args.is_a?(Array) 52 | Ruleby::Ferrari::FunctionBuilder.new(args, block) 53 | else 54 | Ruleby::Ferrari::FunctionBuilder.new([args], block) 55 | end 56 | end 57 | end 58 | 59 | def OR(*args) 60 | Ruleby::Ferrari::OrBuilder.new args 61 | end 62 | 63 | def AND(*args) 64 | Ruleby::Ferrari::AndBuilder.new args 65 | end 66 | 67 | def __eval__(x) 68 | eval(x) 69 | end 70 | 71 | end 72 | end -------------------------------------------------------------------------------- /tests/nil.rb: -------------------------------------------------------------------------------- 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) 2010 Joe Kutner and Matt Smith. All rights reserved. 8 | # 9 | # * Authors: Joe Kutner, Matt Smith 10 | # 11 | 12 | require 'test/unit' 13 | require 'ruleby' 14 | 15 | include Ruleby 16 | 17 | module Nil 18 | 19 | class Number 20 | def initialize(value) 21 | @value = value 22 | end 23 | attr:value, true 24 | end 25 | 26 | class NilRulebook < Rulebook 27 | def rules 28 | rule [Number, :m, m.value == 42], [Context, :c] do |v| 29 | v[:c].inc :rule1 30 | end 31 | 32 | rule [Number, :m, m.value < 42], [Context, :c] do |v| 33 | v[:c].inc :rule2 34 | end 35 | 36 | rule [Number, :m, m.value > 42], [Context, :c] do |v| 37 | v[:c].inc :rule3 38 | end 39 | 40 | rule [Number, :m, m.value <= 42], [Context, :c] do |v| 41 | v[:c].inc :rule4 42 | end 43 | 44 | rule [Number, :m, m.value >= 42], [Context, :c] do |v| 45 | v[:c].inc :rule5 46 | end 47 | end 48 | end 49 | 50 | class Test < Test::Unit::TestCase 51 | 52 | def test_0 53 | 54 | engine :engine do |e| 55 | NilRulebook.new(e).rules 56 | 57 | ctx = Context.new 58 | e.assert ctx 59 | e.assert Number.new(43) 60 | e.assert Number.new(42) 61 | e.assert Number.new(41) 62 | e.match 63 | 64 | assert_equal 1, ctx.get(:rule1) 65 | assert_equal 1, ctx.get(:rule3) 66 | assert_equal 1, ctx.get(:rule2) 67 | assert_equal 2, ctx.get(:rule4) 68 | assert_equal 2, ctx.get(:rule5) 69 | end 70 | end 71 | end 72 | end -------------------------------------------------------------------------------- /tests/duck_type.rb: -------------------------------------------------------------------------------- 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) 2008 Joe Kutner and Matt Smith. All rights reserved. 8 | # 9 | # * Authors: Joe Kutner, Matt Smith, John Mettraux 10 | # 11 | require 'test/unit' 12 | require 'ruleby' 13 | 14 | include Ruleby 15 | 16 | module Duck 17 | class Foobar 18 | end 19 | 20 | class Email < Message 21 | end 22 | 23 | class Loan 24 | def initialize(name,age) 25 | @name = name 26 | @age = age 27 | @status = :HELLO 28 | end 29 | attr :name, true 30 | attr :age, true 31 | attr :status, true 32 | end 33 | 34 | 35 | class DuckRulebook < Rulebook 36 | 37 | def rules 38 | 39 | rule [m.status == :HELLO], [Context, :c] do |v| 40 | v[:c].inc :rule1 41 | end 42 | 43 | rule [:is_a?, Message, m.status == :HELLO], [Context, :c] do |v| 44 | v[:c].inc :rule2 45 | end 46 | 47 | rule [Message, m.status == :HELLO], [Context, :c] do |v| 48 | v[:c].inc :rule3 49 | end 50 | end 51 | end 52 | 53 | class DuckTypeTest < Test::Unit::TestCase 54 | 55 | def test_0 56 | 57 | engine :engine do |e| 58 | DuckRulebook.new(e).rules 59 | ctx = Context.new 60 | a = Loan.new('A','B') 61 | b = Message.new(:HELLO, 'test') 62 | c = Email.new(:HELLO, 'test') 63 | d = Message.new(:FOOBAR, 'foobar') 64 | f = Foobar.new 65 | 66 | e.assert ctx 67 | e.assert a; e.match; e.retract a 68 | e.assert b; e.match; e.retract b 69 | e.assert c; e.match; e.retract c 70 | e.assert d; e.match; e.retract d 71 | e.assert f; e.match; e.retract f 72 | 73 | assert_equal 3, ctx.get(:rule1) 74 | assert_equal 2, ctx.get(:rule2) 75 | assert_equal 1, ctx.get(:rule3) 76 | end 77 | end 78 | end 79 | end -------------------------------------------------------------------------------- /tests/not_patterns.rb: -------------------------------------------------------------------------------- 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) 2008 Joe Kutner and Matt Smith. All rights reserved. 8 | # 9 | # * Authors: Joe Kutner 10 | # 11 | 12 | require 'test/unit' 13 | 14 | require 'ruleby' 15 | 16 | include Ruleby 17 | 18 | module NotPatterns 19 | 20 | class A 21 | attr :name, true 22 | end 23 | 24 | class B 25 | attr :name, true 26 | end 27 | 28 | class C 29 | attr :name, true 30 | end 31 | 32 | class NotPatternsRulebook < Rulebook 33 | def rules 34 | rule [:not, A], [Context, :c] do |v| 35 | v[:c].inc :rule1 36 | end 37 | 38 | rule [Context, :c], [:not, A, m.name == :X] do |v| 39 | v[:c].inc :rule2 40 | end 41 | 42 | rule [:not, A, m.name == :Y], [Context, :c] do |v| 43 | v[:c].inc :rule3 44 | end 45 | 46 | rule [A, {m.name => :x}], 47 | [:not, B, m.name == b(:x)], 48 | [Context, :c] do |v| 49 | v[:c].inc :rule4 50 | end 51 | 52 | rule [A, :a, {m.name => :x}], 53 | [:not, C, :c, m.name == b(:x)], 54 | [Context, :c] do |v| 55 | v[:c].inc :rule5 56 | end 57 | 58 | rule [:not, Message], [Context, :c] do |v| 59 | v[:c].inc :rule6 60 | end 61 | end 62 | end 63 | 64 | class Test < Test::Unit::TestCase 65 | def test_0 66 | engine :engine do |e| 67 | NotPatternsRulebook.new(e).rules 68 | ctx = Context.new 69 | e.assert ctx 70 | 71 | a = A.new 72 | a.name = :X 73 | e.assert a 74 | b = B.new 75 | b.name = :Y 76 | e.assert b 77 | c = C.new 78 | c.name = :X 79 | e.assert c 80 | 81 | e.match 82 | assert_equal 0, ctx.get(:rule1) 83 | assert_equal 1, ctx.get(:rule3) 84 | assert_equal 0, ctx.get(:rule2) 85 | assert_equal 1, ctx.get(:rule4) 86 | assert_equal 0, ctx.get(:rule5) 87 | assert_equal 1, ctx.get(:rule6) 88 | end 89 | end 90 | end 91 | end -------------------------------------------------------------------------------- /examples/fibonacci_example3.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/') 13 | require 'ruleby' 14 | require 'rule_helper' 15 | class Fibonacci 16 | def initialize(sequence,value=-1) 17 | @sequence = sequence 18 | @value = value 19 | end 20 | 21 | attr_reader :sequence 22 | attr :value, true 23 | 24 | def to_s 25 | return super + "::sequence=" + @sequence.to_s + ",value=" + @value.to_s 26 | end 27 | end 28 | 29 | include Ruleby::RuleHelper 30 | #RULES 31 | rules = [] 32 | # Bootstrap1 33 | rules += rule :Bootstrap1, {:priority => 4}, 34 | [Fibonacci, :f, m.value == -1, m.sequence == 1 ] do |vars, engine| 35 | vars[:f].value = 1 36 | engine.modify vars[:f] 37 | puts vars[:f].sequence.to_s + ' == ' + vars[:f].value.to_s 38 | end 39 | 40 | # Recurse 41 | rules += rule :Recurse, {:priority => 3}, 42 | [Fibonacci, :f, m.value == -1] do |vars, engine| 43 | f2 = Fibonacci.new(vars[:f].sequence - 1) 44 | engine.assert f2 45 | puts 'recurse for ' + f2.sequence.to_s 46 | end 47 | 48 | # Bootstrap2 49 | rules += rule :Bootstrap2, 50 | [Fibonacci, :f, m.value == -1 , m.sequence == 2] do |vars, engine| 51 | vars[:f].value = 1 52 | engine.modify vars[:f] 53 | puts vars[:f].sequence.to_s + ' == ' + vars[:f].value.to_s 54 | end 55 | 56 | # Calculate 57 | rules += rule :Calculate, 58 | [Fibonacci,:f1, m.value.not== -1, {m.sequence => :s1}], 59 | [Fibonacci,:f2, m.value.not== -1, {m.sequence( :s1, &c{ |s2,s1| s2 == s1 + 1 } ) => :s2}], 60 | [Fibonacci,:f3, m.value == -1, m.sequence(:s2, &c{ |s3,s2| s3 == s2 + 1 }) ] do |vars, engine| 61 | vars[:f3].value = vars[:f1].value + vars[:f2].value 62 | engine.modify vars[:f3] 63 | engine.retract vars[:f1] 64 | puts vars[:f3].sequence.to_s + ' == ' + vars[:f3].value.to_s 65 | end 66 | 67 | # FACTS 68 | fib1 = Fibonacci.new(150) 69 | 70 | include Ruleby 71 | 72 | engine :engine do |e| 73 | rules.each do |r| 74 | e.assert_rule r 75 | end 76 | e.assert fib1 77 | e.match 78 | end -------------------------------------------------------------------------------- /lib/rulebook.rb: -------------------------------------------------------------------------------- 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, Joe Kutner 10 | # 11 | 12 | require 'ruleby' 13 | require 'rule_helper' 14 | require 'dsl/ferrari' 15 | 16 | module Ruleby 17 | class Rulebook 18 | include Ruleby 19 | include Ruleby::RuleHelper 20 | def initialize(engine, &block) 21 | @engine = engine 22 | yield self if block_given? 23 | end 24 | 25 | attr_reader :engine 26 | 27 | def assert(fact) 28 | @engine.assert fact 29 | end 30 | def retract(fact) 31 | @engine.retract fact 32 | end 33 | def modify(fact) 34 | @engine.modify fact 35 | end 36 | def rule(*args, &block) 37 | if args.empty? 38 | raise 'Must provide arguments to rule' 39 | else 40 | name = args[0].kind_of?(Symbol) ? args.shift : GeneratedTag.new 41 | i = args[0].kind_of?(Hash) ? 1 : 0 42 | if [Array, Ruleby::Ferrari::OrBuilder, Ruleby::Ferrari::AndBuilder].include? args[i].class 43 | # use ferrari DSL 44 | r = Ferrari::RulebookHelper.new @engine 45 | r.rule name, *args, &block 46 | elsif args[i].kind_of? String 47 | # use letigre DSL 48 | r = LeTigre::RulebookHelper.new @engine, self 49 | r.rule name, *args, &block 50 | else 51 | raise 'Rule format not recognized.' 52 | end 53 | end 54 | end 55 | end 56 | 57 | class GeneratedTag 58 | # this counter is incremented for each UniqueTag created, and is 59 | # appended to the end of the unique_seed in order to create a 60 | # string that is unique for each instance of this class. 61 | @@tag_counter = 0 62 | 63 | # every generated tag will be prefixed with this string. This isn't full-proof. 64 | @@unique_seed = 'unique_seed' 65 | 66 | def initialize() 67 | @@tag_counter += 1 68 | @tag = @@unique_seed + @@tag_counter.to_s 69 | end 70 | 71 | attr_reader:tag_counter 72 | attr_reader:unique_seed 73 | attr_reader:tag 74 | 75 | def ==(ut) 76 | return ut && ut.kind_of?(GeneratedTag) && @tag == ut.tag 77 | end 78 | 79 | def to_s 80 | return @tag.to_s 81 | end 82 | end 83 | end -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Ruleby is copyrighted free software by Joe Kutner and Matt Smith. You can 2 | redistribute it and/or modify it under either the terms of the GPL (see the 3 | GPL.txt file), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a) place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b) use the modified software only within your corporation or 18 | organization. 19 | 20 | c) rename any non-standard executables so the names do not conflict 21 | with standard executables, which must also be provided. 22 | 23 | d) make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or executable 26 | form, provided that you do at least ONE of the following: 27 | 28 | a) distribute the executables and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b) accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c) give non-standard executables non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d) make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). 42 | 43 | 5. The scripts and library files supplied as input to or produced as 44 | output from the software do not automatically fall under the 45 | copyright of the software, but belong to whomever generated them, 46 | and may be sold commercially, and may be aggregated with this 47 | software. 48 | 49 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 50 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 51 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 52 | PURPOSE. -------------------------------------------------------------------------------- /examples/fibonacci_rulebook.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | require 'ruleby' 13 | 14 | include Ruleby 15 | 16 | # NOTE this example uses the LeTigre DSL syntax. In addition, its semantics are 17 | # different from the other classes. 18 | class FibonacciRulebook2 < Rulebook 19 | MAX_SEQUENCE = 100 20 | def rules 21 | rule :Calculate, {:priority => 2 }, 22 | 'Fibonacci as :f1 where #value != -1 #&& #sequence as :s1', 23 | 'Fibonacci as :f2 where #value != -1 #&& #sequence == #:s1 + 1 as :s2', 24 | 'Fibonacci as :f3 where #value == -1 #&& #sequence == #:s2 + 1' do |vars| 25 | retract vars[:f1] 26 | retract vars[:f3] 27 | if(vars[:f2].sequence == MAX_SEQUENCE) 28 | retract vars[:f2] 29 | else 30 | f3 = Fibonacci.new(vars[:f2].sequence + 1, vars[:f1].value + vars[:f2].value) 31 | assert f3 32 | puts "#{f3.sequence} == #{f3.value}" 33 | end 34 | end 35 | 36 | rule :Build, {:priority => 1}, 37 | 'Fibonacci as :f1 where #value != -1 #&& #sequence as :s1', 38 | 'Fibonacci as :f2 where #value != -1 #&& #sequence == #:s1 + 1' do |vars| 39 | f3 = Fibonacci.new(vars[:f2].sequence + 1, -1) 40 | assert f3 41 | end 42 | end 43 | end 44 | 45 | # NOTE 46 | # In this class we demonstrate the Ferrari DSL syntax. 47 | class FibonacciRulebookFerrari < Rulebook 48 | def rules 49 | # Bootstrap1 50 | rule :Bootstrap1, {:priority => 4}, 51 | [Fibonacci, :f, m.value == -1, m.sequence == 1 ] do |vars| 52 | vars[:f].value = 1 53 | modify vars[:f] 54 | puts vars[:f].sequence.to_s + ' == ' + vars[:f].value.to_s 55 | end 56 | 57 | # Recurse 58 | rule :Recurse, {:priority => 3}, 59 | [Fibonacci, :f, m.value == -1] do |vars| 60 | f2 = Fibonacci.new(vars[:f].sequence - 1) 61 | assert f2 62 | puts 'recurse for ' + f2.sequence.to_s 63 | end 64 | 65 | # Bootstrap2 66 | rule :Bootstrap2, 67 | [Fibonacci, :f, m.value == -1 , m.sequence == 2] do |vars| 68 | vars[:f].value = 1 69 | modify vars[:f] 70 | puts vars[:f].sequence.to_s + ' == ' + vars[:f].value.to_s 71 | end 72 | 73 | # Calculate 74 | rule :Calculate, 75 | [Fibonacci,:f1, m.value.not== -1, {m.sequence => :s1}], 76 | [Fibonacci,:f2, m.value.not== -1, {m.sequence( :s1, &c{ |s2,s1| s2 == s1 + 1 } ) => :s2}], 77 | [Fibonacci,:f3, m.value == -1, m.sequence(:s2, &c{ |s3,s2| s3 == s2 + 1 }) ] do |vars| 78 | vars[:f3].value = vars[:f1].value + vars[:f2].value 79 | modify vars[:f3] 80 | retract vars[:f1] 81 | puts vars[:f3].sequence.to_s + ' == ' + vars[:f3].value.to_s 82 | end 83 | end 84 | end -------------------------------------------------------------------------------- /lib/core/patterns.rb: -------------------------------------------------------------------------------- 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 Core 14 | 15 | class Pattern 16 | end 17 | 18 | # This class represents a pattern that is looking for the existence of some 19 | # object. It contains a list of 'atoms' that represent the properties of 20 | # the class that we are looking for. 21 | class ObjectPattern < Pattern 22 | attr_reader :atoms 23 | 24 | def initialize(head, atoms) 25 | @atoms = [head] + atoms 26 | end 27 | 28 | def head 29 | @atoms[0] 30 | end 31 | 32 | def ==(pattern) 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 40 | end 41 | return true 42 | end 43 | end 44 | return false 45 | end 46 | 47 | def to_s 48 | return '(' + @atoms.join('|') + ')' 49 | end 50 | end 51 | 52 | class InheritsPattern < ObjectPattern 53 | end 54 | 55 | class CollectPattern < ObjectPattern 56 | end 57 | 58 | # This class represents a pattern that is looking for the absence of some 59 | # object (rather than the existence of). In all respects, it is the same as 60 | # an ObjectPattern, but it is handled differently by the inference engine. 61 | class NotPattern < ObjectPattern 62 | end 63 | 64 | class NotInheritsPattern < InheritsPattern 65 | end 66 | 67 | # A composite pattern represents a logical conjunction of two patterns. The 68 | # inference engine interprets this differently from an ObjectPattern because 69 | # it simply aggregates patterns. 70 | class CompositePattern < Pattern 71 | 72 | attr_reader :left_pattern 73 | attr_reader :right_pattern 74 | 75 | def initialize(left_pattern, right_pattern) 76 | @left_pattern = left_pattern 77 | @right_pattern = right_pattern 78 | end 79 | 80 | def atoms 81 | atoms = [] 82 | atoms.push @left_pattern.atoms 83 | atoms.push @right_pattern.atoms 84 | return atoms 85 | end 86 | end 87 | 88 | class AndPattern < CompositePattern 89 | 90 | def initialize(left_pattern, right_pattern) 91 | super(left_pattern, right_pattern) 92 | @head = :and 93 | end 94 | 95 | end 96 | 97 | class OrPattern < CompositePattern 98 | 99 | def initialize(left_pattern, right_pattern) 100 | super(left_pattern, right_pattern) 101 | @head = :or 102 | end 103 | 104 | end 105 | 106 | class InitialFactPattern < ObjectPattern 107 | def initialize 108 | deftemplate = Template.new InitialFact, :equals 109 | htag = GeneratedTag.new 110 | head = HeadAtom.new htag, deftemplate 111 | super(head, []) 112 | end 113 | end 114 | 115 | class PatternFactory 116 | # TODO add some convenience methods for creating patterns 117 | end 118 | end 119 | end -------------------------------------------------------------------------------- /examples/ticket.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/') 13 | require 'ruleby' 14 | 15 | include Ruleby 16 | 17 | class Customer 18 | def initialize(name,subscription) 19 | @name = name 20 | @subscription = subscription 21 | end 22 | attr_reader :name,:subscription 23 | def to_s 24 | return '[Customer ' + @name.to_s + ' : ' + @subscription.to_s + ']'; 25 | end 26 | end 27 | 28 | class Ticket 29 | def initialize(customer) 30 | @customer = customer 31 | @status = :New 32 | end 33 | attr :status, true 34 | attr_reader :customer 35 | def to_s 36 | return '[Ticket ' + @customer.to_s + ' : ' + @status.to_s + ']'; 37 | end 38 | end 39 | 40 | # This example is used in JBoss-Rules to demonstrate durations and the use of 41 | # custom DSL. We are simply using it here to demonstrate another example. 42 | class TroubleTicketRulebook < Rulebook 43 | def rules 44 | 45 | # This is uses the letigre syntax... but we can mix and match syntaxes in 46 | # the same rule set. 47 | rule :New_Ticket, {:priority => 10}, # :duration => 10}, 48 | [Customer, :c], 49 | [Ticket, :ticket, {m.customer => :c}, m.status == :New] do |vars| 50 | puts 'New : ' + vars[:ticket].to_s 51 | end 52 | 53 | # Now we are using the ferrari syntax. The rule method can detect which 54 | # syntax we are using, and compile accordingly. 55 | rule :Silver_Priority, #{:duration => 3000}, 56 | [Customer, :customer, m.subscription == 'Silver'], 57 | [Ticket,:ticket, m.customer == b(:customer), m.status == :New] do |vars| 58 | vars[:ticket].status = :Escalate 59 | modify vars[:ticket] 60 | end 61 | 62 | rule :Gold_Priority, #{:duration => 1000}, 63 | [Customer, :customer, m.subscription == 'Gold'], 64 | [Ticket,:ticket, m.customer == b(:customer), m.status == :New] do |vars| 65 | vars[:ticket].status = :Escalate 66 | modify vars[:ticket] 67 | end 68 | 69 | rule :Platinum_Priority, 70 | [Customer, :customer, m.subscription == 'Platinum'], 71 | [Ticket,:ticket, m.customer == b(:customer), m.status == :New] do |vars| 72 | vars[:ticket].status = :Escalate 73 | modify vars[:ticket] 74 | end 75 | 76 | rule :Escalate, 77 | [Customer, :c], 78 | [Ticket, :ticket, {m.customer => :c}, m.status == :Escalate] do |vars| 79 | puts 'Email : ' + vars[:ticket].to_s 80 | end 81 | 82 | rule :Done, 83 | [Customer, :c], 84 | [Ticket, :ticket, {m.customer => :c}, m.status == :Done] do |vars| 85 | puts 'Done : ' + vars[:ticket].to_s 86 | end 87 | end 88 | end 89 | 90 | # FACTS 91 | 92 | a = Customer.new('A', 'Gold') 93 | b = Customer.new('B', 'Platinum') 94 | c = Customer.new('C', 'Silver') 95 | d = Customer.new('D', 'Silver') 96 | 97 | t1 = Ticket.new(a) 98 | t2 = Ticket.new(b) 99 | t3 = Ticket.new(c) 100 | t4 = Ticket.new(d) 101 | 102 | engine :engine do |e| 103 | TroubleTicketRulebook.new(e).rules 104 | e.assert a 105 | e.assert b 106 | e.assert c 107 | e.assert d 108 | e.assert t1 109 | e.assert t2 110 | e.assert t3 111 | e.assert t4 112 | e.match 113 | end -------------------------------------------------------------------------------- /tests/assert_facts.rb: -------------------------------------------------------------------------------- 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) 2008 Joe Kutner and Matt Smith. All rights reserved. 8 | # 9 | # * Authors: Joe Kutner 10 | # 11 | 12 | require 'test/unit' 13 | 14 | require 'ruleby' 15 | 16 | include Ruleby 17 | 18 | module AssertFacts 19 | 20 | class Fibonacci 21 | def initialize(sequence,value=-1) 22 | @sequence = sequence 23 | @value = value 24 | end 25 | 26 | attr_reader :sequence 27 | attr :value, true 28 | 29 | def to_s 30 | return super + "::sequence=" + @sequence.to_s + ",value=" + @value.to_s 31 | end 32 | end 33 | 34 | class SimpleRulebook < Rulebook 35 | def rules 36 | rule [Message, :m, {m.message => :x}, m.status == b(:x)], 37 | [Context, :c] do |v| 38 | v[:c].inc :rule1 39 | end 40 | end 41 | end 42 | 43 | class FibonacciRulebook < Rulebook 44 | def rules 45 | # Bootstrap1 46 | rule :Bootstrap1, {:priority => 4}, 47 | [Fibonacci, :f, m.value == -1, m.sequence == 1 ] do |vars| 48 | vars[:f].value = 1 49 | modify vars[:f] 50 | end 51 | 52 | # Recurse 53 | rule :Recurse, {:priority => 3}, 54 | [Fibonacci, :f, m.value == -1] do |vars| 55 | f2 = Fibonacci.new(vars[:f].sequence - 1) 56 | assert f2 57 | end 58 | 59 | # Bootstrap2 60 | rule :Bootstrap2, 61 | [Fibonacci, :f, m.value == -1 , m.sequence == 2] do |vars| 62 | vars[:f].value = 1 63 | modify vars[:f] 64 | end 65 | 66 | # Calculate 67 | rule :Calculate, 68 | [Context, :c], 69 | [Fibonacci,:f1, m.value.not== -1, {m.sequence => :s1}], 70 | [Fibonacci,:f2, m.value.not== -1, {m.sequence( :s1, &c{ |s2,s1| s2 == s1 + 1 } ) => :s2}], 71 | [Fibonacci,:f3, m.value == -1, m.sequence(:s2, &c{ |s3,s2| s3 == s2 + 1 }) ] do |vars| 72 | vars[:f3].value = vars[:f1].value + vars[:f2].value 73 | modify vars[:f3] 74 | retract vars[:f1] 75 | vars[:c].set vars[:f3].sequence, vars[:f3].value 76 | end 77 | end 78 | end 79 | 80 | class Test < Test::Unit::TestCase 81 | 82 | def test_0 83 | engine :engine do |e| 84 | ctx = Context.new 85 | e.assert ctx 86 | e.assert Message.new(:HELLO, :HELLO) 87 | SimpleRulebook.new(e).rules 88 | e.assert Message.new(:HELLO, :GOODBYE) 89 | e.match 90 | assert_equal 1, ctx.get(:rule1) 91 | end 92 | end 93 | 94 | def test_1 95 | engine :engine do |e| 96 | ctx = Context.new 97 | e.assert ctx 98 | e.assert Message.new(:HELLO, :HELLO) 99 | e.assert Message.new(:HELLO, :GOODBYE) 100 | SimpleRulebook.new(e).rules 101 | e.match 102 | assert_equal 1, ctx.get(:rule1) 103 | end 104 | end 105 | 106 | def test_2 107 | fib1 = Fibonacci.new(150) 108 | engine :engine do |e| 109 | FibonacciRulebook.new(e).rules 110 | ctx = Context.new 111 | e.assert ctx 112 | e.assert fib1 113 | e.match 114 | assert_equal 9969216677189303386214405760200, ctx.get(150) 115 | end 116 | end 117 | 118 | def test_3 119 | fib1 = Fibonacci.new(150) 120 | engine :engine do |e| 121 | ctx = Context.new 122 | e.assert ctx 123 | e.assert fib1 124 | FibonacciRulebook.new(e).rules 125 | e.match 126 | assert_equal 9969216677189303386214405760200, ctx.get(150) 127 | end 128 | end 129 | end 130 | end -------------------------------------------------------------------------------- /examples/diagnosis.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/') 13 | require 'ruleby' 14 | 15 | class Patient 16 | def initialize(name,fever,spots,rash,sore_throat,innoculated) 17 | @name = name 18 | @fever = fever 19 | @spots= spots 20 | @rash = rash 21 | @sore_throat = sore_throat 22 | @innoculated = innoculated 23 | end 24 | attr:name, true 25 | attr:fever, true 26 | attr:spots, true 27 | attr:rash, true 28 | attr:sore_throat, true 29 | attr:innoculated, true 30 | end 31 | 32 | class Diagnosis 33 | def initialize(name,diagnosis) 34 | @name=name 35 | @diagnosis=diagnosis 36 | end 37 | attr:name,true 38 | attr:diagnosis,true 39 | end 40 | 41 | class Treatment 42 | def initialize(name,treatment) 43 | @name=name 44 | @treatment=treatment 45 | end 46 | attr:name,true 47 | attr:treatment,true 48 | end 49 | 50 | class DiagnosisRulebook < Ruleby::Rulebook 51 | def rules 52 | rule :Measles, {:priority => 100}, 53 | [Patient,:p,{m.name=>:n},m.fever==:high,m.spots==true,m.innoculated==true] do |v| 54 | name = v[:n] 55 | assert Diagnosis.new(name, :measles) 56 | puts "Measles diagnosed for #{name}" 57 | end 58 | 59 | rule :Allergy1, 60 | [Patient,:p, {m.name=>:n}, m.spots==true], 61 | [:not, Diagnosis, m.name==b(:n), m.diagnosis==:measles] do |v| 62 | name = v[:n] 63 | assert Diagnosis.new(name, :allergy) 64 | puts "Allergy diagnosed for #{name} from spots and lack of measles" 65 | end 66 | 67 | rule :Allergy2, 68 | [Patient,:p, {m.name=>:n}, m.rash==true] do |v| 69 | name = v[:n] 70 | assert Diagnosis.new(name, :allergy) 71 | puts "Allergy diagnosed from rash for #{name}" 72 | end 73 | 74 | rule :Flu, 75 | [Patient,:p, {m.name=>:n}, m.sore_throat==true, m.fever(&c{|f| f==:mild || f==:high})] do |v| 76 | name = v[:n] 77 | assert Diagnosis.new(name, :flu) 78 | puts "Flu diagnosed for #{name}" 79 | end 80 | 81 | rule :Penicillin, 82 | [Diagnosis, :d, {m.name => :n}, m.diagnosis==:measles] do |v| 83 | name = v[:n] 84 | assert Treatment.new(name, :penicillin) 85 | puts "Penicillin prescribed for #{name}" 86 | end 87 | 88 | rule :Allergy_pills, 89 | [Diagnosis, :d, {m.name => :n}, m.diagnosis==:allergy] do |v| 90 | name = v[:n] 91 | assert Treatment.new(name, :allergy_shot) 92 | puts "Allergy shot prescribed for #{name}" 93 | end 94 | 95 | rule :Bed_rest, 96 | [Diagnosis, :d, {m.name => :n}, m.diagnosis==:flu] do |v| 97 | name = v[:n] 98 | assert Treatment.new(name, :bed_rest) 99 | puts "Bed rest prescribed for #{name}" 100 | end 101 | end 102 | end 103 | 104 | include Ruleby 105 | 106 | engine :engine do |e| 107 | 108 | DiagnosisRulebook.new e do |r| 109 | r.rules 110 | end 111 | 112 | e.assert Patient.new('Fred',:none,true,false,false,false) 113 | e.assert Patient.new('Joe',:high,false,false,true,false) 114 | e.assert Patient.new('Bob',:high,true,false,false,true) 115 | e.assert Patient.new('Tom',:none,false,true,false,false) 116 | 117 | e.match 118 | 119 | # expect this output: 120 | # Measles diagnosed for Bob 121 | # Penicillin prescribed for Bob 122 | # Allergy diagnosed from rash for Tom 123 | # Allergy shot prescribed for Tom 124 | # Flu diagnosed for Joe 125 | # Bed rest prescribed for Joe 126 | # Allergy diagnosed for Fred from spots and lack of measles 127 | # Allergy shot prescribed for Fred 128 | 129 | end -------------------------------------------------------------------------------- /examples/wordgame.rb: -------------------------------------------------------------------------------- 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 | 13 | # This example solves the number puzzle problem where 14 | # GERALD 15 | # + DONALD 16 | # ------ 17 | # = ROBERT 18 | 19 | $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/') 20 | require 'ruleby' 21 | 22 | include Ruleby 23 | 24 | class Combination 25 | attr:a 26 | attr:x 27 | def initialize(a,x) 28 | @a = a 29 | @x = x 30 | end 31 | end 32 | 33 | class WordGameRulebook < Ruleby::Rulebook 34 | def rules 35 | rule :generate_combos, [String, :a], [Fixnum, :x] do |v| 36 | assert Combination.new(v[:a], v[:x]) 37 | end 38 | 39 | c1 = c{|t,d| ((d+d) % 10) == t } 40 | c2 = c{|r,d,t,l| ((d+d+(10*l)+(10*l)) % 100) == ((10 * r) + t) } 41 | c3 = c{|e,d,l,a,r,t| ((d+d+(10*l)+(10*l)+(100*a)+(100*a)) % 1000) == ((100*e)+(10*r)+t) } 42 | c4 = c{|b,d,l,a,r,n,e,t| ((d+d+(10*l)+(10*l)+(100*a)+(100*a)+(1000*r)+(1000*n)) % 10000) == ((1000*b)+(100*e)+(10*r)+t) } 43 | c5 = c{|g,d,l,a,r,n,e,o,b,t| (d+d+(10*l)+(10*l)+(100*a)+(100*a)+(1000*r)+(1000*n)+(10000*e)+(10000*o)+(100000*g)+(100000*d)) == ((100000*r)+(10000*o)+(1000*b)+(100*e)+(10*r)+t) } 44 | 45 | rule :find_solution, 46 | [Combination, m.a=='D', {m.x => :d}], 47 | [Combination, m.a=='T', {m.x.not==b(:d)=>:t}, m.x(:d, &c1)], 48 | [Combination, m.a=='L', {m.x.not==b(:d)=>:l}, m.x.not==b(:t)], 49 | [Combination, m.a=='R', {m.x.not==b(:d)=>:r}, m.x.not==b(:t), m.x.not==b(:l), 50 | m.x(:d,:t,:l, &c2)], 51 | [Combination, m.a=='A', {m.x.not==b(:d)=>:a}, m.x.not==b(:t), m.x.not==b(:l), m.x.not==b(:r)], 52 | [Combination, m.a=='E', {m.x.not==b(:d)=>:e}, m.x.not==b(:t), m.x.not==b(:l), m.x.not==b(:r), 53 | m.x.not==b(:a), m.x(:d,:l,:a,:r,:t, &c3)], 54 | [Combination, m.a=='N', {m.x.not==b(:d)=>:n}, m.x.not==b(:t), m.x.not==b(:l), m.x.not==b(:r), 55 | m.x.not==b(:a), m.x.not==b(:e)], 56 | [Combination, m.a=='B', {m.x.not==b(:d)=>:b}, m.x.not==b(:t), m.x.not==b(:l), m.x.not==b(:r), 57 | m.x.not==b(:a), m.x.not==b(:e), m.x.not==b(:n), m.x(:d,:l,:a,:r,:n,:e,:t, &c4)], 58 | [Combination, m.a=='O', {m.x.not==b(:d)=>:o}, m.x.not==b(:t), m.x.not==b(:l), m.x.not==b(:r), 59 | m.x.not==b(:a), m.x.not==b(:e), m.x.not==b(:n), m.x.not==b(:b)], 60 | [Combination, m.a=='G', {m.x.not==b(:d)=>:g}, m.x.not==b(:t), m.x.not==b(:l), m.x.not==b(:r), 61 | m.x.not==b(:a), m.x.not==b(:e), m.x.not==b(:n), m.x.not==b(:b), m.x.not==b(:o), 62 | m.x(:d,:l,:a,:r,:n,:e,:o,:b,:t, &c5)] do |v| 63 | puts "One Solution is:" 64 | puts " G = #{v[:g]}" 65 | puts " E = #{v[:e]}" 66 | puts " R = #{v[:r]}" 67 | puts " A = #{v[:a]}" 68 | puts " L = #{v[:l]}" 69 | puts " D = #{v[:d]}" 70 | puts " O = #{v[:o]}" 71 | puts " N = #{v[:n]}" 72 | puts " B = #{v[:b]}" 73 | puts " T = #{v[:t]}" 74 | puts "" 75 | puts " #{v[:g]} #{v[:e]} #{v[:r]} #{v[:a]} #{v[:l]} #{v[:d]}" 76 | puts " + #{v[:d]} #{v[:o]} #{v[:n]} #{v[:a]} #{v[:l]} #{v[:d]}" 77 | puts " ------" 78 | puts " = #{v[:r]} #{v[:o]} #{v[:b]} #{v[:e]} #{v[:r]} #{v[:t]}" 79 | end 80 | end 81 | end 82 | 83 | e = engine :e do |e| 84 | WordGameRulebook.new(e).rules 85 | e.assert 0 86 | e.assert 1 87 | e.assert 2 88 | e.assert 3 89 | e.assert 4 90 | e.assert 5 91 | e.assert 6 92 | e.assert 7 93 | e.assert 8 94 | e.assert 9 95 | e.assert 'G' 96 | e.assert 'E' 97 | e.assert 'R' 98 | e.assert 'A' 99 | e.assert 'L' 100 | e.assert 'D' 101 | e.assert 'O' 102 | e.assert 'N' 103 | e.assert 'B' 104 | e.assert 'T' 105 | end 106 | 107 | e.match -------------------------------------------------------------------------------- /spec/errors_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class A 4 | attr :value, true 5 | def initialize(v=nil); @value = v; end 6 | end 7 | 8 | include Ruleby 9 | 10 | class ErrorsRulebook < Rulebook 11 | def rules_with_method_that_doesnt_exist 12 | rule [A, :a, m.foobar == 'quack'] do |v| 13 | assert Success.new 14 | end 15 | end 16 | 17 | def rules_that_raise_errors 18 | rule [A, :a, m.value(&c{|v| raise ":(" if v == 42; true})] do |v| 19 | assert Success.new 20 | end 21 | end 22 | 23 | end 24 | 25 | describe Ruleby::Core::Engine do 26 | 27 | describe "#errors" do 28 | context "rules_with_method_that_doesnt_exist" do 29 | subject do 30 | engine :engine do |e| 31 | ErrorsRulebook.new(e).rules_with_method_that_doesnt_exist 32 | end 33 | end 34 | 35 | context "with one A" do 36 | before do 37 | subject.assert A.new 38 | subject.match 39 | end 40 | 41 | it "should accumulate an error" do 42 | r = subject.retrieve Success 43 | r.size.should == 0 44 | 45 | errors = subject.errors 46 | errors.should_not be_nil 47 | errors.size.should == 1 48 | errors[0].type.should == :no_method 49 | errors[0].details[:method].should == :foobar 50 | errors[0].details[:object].should match /#/ 51 | subject.clear_errors 52 | 53 | subject.errors.should == [] 54 | end 55 | end 56 | 57 | context "with one A that quacks" do 58 | before do 59 | a = A.new 60 | 61 | # define a method on the instance (not on the class) 62 | def a.foobar 63 | "quack" 64 | end 65 | 66 | subject.assert a 67 | subject.assert A.new 68 | subject.match 69 | end 70 | 71 | it "should accumulate one error" do 72 | r = subject.retrieve Success 73 | r.size.should == 1 74 | 75 | errors = subject.errors 76 | errors.should_not be_nil 77 | errors.size.should == 1 78 | errors[0].type.should == :no_method 79 | errors[0].details[:method].should == :foobar 80 | errors[0].details[:object].should match /#/ 81 | subject.clear_errors 82 | end 83 | end 84 | end 85 | 86 | 87 | context "rules_that_raise_errors" do 88 | subject do 89 | engine :engine do |e| 90 | ErrorsRulebook.new(e).rules_that_raise_errors 91 | end 92 | end 93 | 94 | context "with one A where value==42" do 95 | before do 96 | a = A.new 97 | a.value = 42 98 | subject.assert a 99 | subject.match 100 | end 101 | 102 | it "should accumulate an error" do 103 | r = subject.retrieve Success 104 | r.size.should == 0 105 | 106 | errors = subject.errors 107 | errors.should_not be_nil 108 | errors.size.should == 1 109 | errors[0].type.should == :proc_call 110 | errors[0].details[:message].should == ':(' 111 | errors[0].details[:object].should match /#/ 112 | errors[0].details[:method].should == :value 113 | errors[0].details[:value].should == "42" 114 | subject.clear_errors 115 | end 116 | end 117 | 118 | context "with one A where value==42, and one A where value==0" do 119 | before do 120 | a1 = A.new 121 | a1.value = 42 122 | 123 | a2 = A.new 124 | a2.value = 0 125 | 126 | subject.assert a1 127 | subject.assert a2 128 | subject.match 129 | end 130 | 131 | it "should accumulate one error" do 132 | r = subject.retrieve Success 133 | r.size.should == 1 134 | 135 | errors = subject.errors 136 | errors.should_not be_nil 137 | errors.size.should == 1 138 | errors[0].type.should == :proc_call 139 | errors[0].details[:message].should == ':(' 140 | errors[0].details[:object].should match /#/ 141 | errors[0].details[:method].should == :value 142 | errors[0].details[:value].should == "42" 143 | subject.clear_errors 144 | end 145 | end 146 | end 147 | end 148 | end -------------------------------------------------------------------------------- /benchmarks/miss_manners/rules.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | require 'model' 13 | 14 | module MissManners 15 | 16 | class MannersRulebook < Ruleby::Rulebook 17 | def rules 18 | rule :assignFirstSeat, 19 | [Context,:context, m.state == Context::START_UP], 20 | [Guest,:guest], 21 | [Count,:count] do |vars| 22 | guestName = vars[:guest].name 23 | seating = Seating.new(vars[:count].value,1,true,1,guestName,1,guestName) 24 | assert seating 25 | path = Path.new(vars[:count].value, 1, guestName) 26 | assert path 27 | retract vars[:count] 28 | vars[:count].value = vars[:count].value + 1 29 | assert vars[:count] 30 | puts "assign first seat : #{seating.to_s} : #{path.to_s}" 31 | vars[:context].state = Context::ASSIGN_SEATS 32 | modify vars[:context] 33 | end 34 | 35 | rule :findSeating, 36 | [Context,:context, {m.state == Context::ASSIGN_SEATS => :state}], 37 | [Count,:count, {m.value => :countValue}], 38 | [Seating,:seating, {m.path == true =>:p, m.id=>:seatingId, m.pid=>:seatingPid, m.rightSeat=>:seatingRightSeat, m.rightGuestName=>:seatingRightGuestName}], 39 | [Guest,:g, {m.name=>:name, m.sex=>:rightGuestSex, m.hobby=>:rightGuestHobby}, m.name == b(:seatingRightGuestName)], 40 | [Guest,:lg, {m.name=>:leftGuestName, m.sex=>:sex, m.hobby == b(:rightGuestHobby) => :hobby}, m.sex(:rightGuestSex, &c{|s,rgs| s != rgs} )], 41 | [:~,Path, m.id == b(:seatingId), m.guestName == b(:leftGuestName)], 42 | [:~,Chosen, m.id == b(:seatingId), m.guestName == b(:leftGuestName), m.hobby == b(:leftGuestHobby)] do |vars| 43 | rightSeat = vars[:seatingRightSeat] 44 | seatId = vars[:seatingId] 45 | countValue = vars[:count].value 46 | seating = Seating.new(countValue,seatId,false,rightSeat,vars[:seatingRightGuestName],rightSeat+1,vars[:leftGuestName]) 47 | path = Path.new(countValue,rightSeat+1,vars[:leftGuestName]) 48 | chosen = Chosen.new(seatId, vars[:leftGuestName], vars[:rightGuestHobby] ) 49 | puts "find seating : #{seating} : #{path} : #{chosen}" 50 | assert seating 51 | assert path 52 | assert chosen 53 | vars[:count].value = countValue + 1 54 | modify vars[:count] 55 | vars[:context].state = Context::MAKE_PATH 56 | modify vars[:context] 57 | end 58 | 59 | rule :makePath, 60 | [Context,:context, {m.state == Context::MAKE_PATH => :s}], 61 | [Seating,:seating, {m.id=>:seatingId, m.pid=>:seatingPid, m.path == false =>:p}], 62 | [Path,:path, {m.guestName=>:pathGuestName, m.seat=>:pathSeat}, m.id == b(:seatingPid)], 63 | [:~,Path,m.id == b(:seatingId), m.guestName == b(:pathGuestName)] do |vars| 64 | path = Path.new(vars[:seatingId],vars[:pathSeat],vars[:pathGuestName]) 65 | assert path 66 | puts "make Path : #{path}" 67 | end 68 | 69 | # NOTE We had to add the priority because Ruleby's conflict resolution strategy 70 | # is not robust enough. If it worked like CLIPS, the priority would not 71 | # be nessecary because the 'make path' activations would have more 72 | # recent facts supporting it. This is really an error in the Miss Manners 73 | # benchmark, so it is not considered cheating. 74 | rule :pathDone, {:priority => -5}, 75 | [Context,:context, m.state == Context::MAKE_PATH], 76 | [Seating,:seating, m.path == false] do |vars| 77 | vars[:seating].path = true 78 | modify vars[:seating] 79 | vars[:context].state = Context::CHECK_DONE 80 | modify vars[:context] 81 | puts "path Done : #{vars[:seating]}" 82 | end 83 | 84 | rule :areWeDone, 85 | [Context,:context, m.state == Context::CHECK_DONE], 86 | [LastSeat,:ls, {m.seat => :lastSeat}], 87 | [Seating,:seating, m.rightSeat == b(:lastSeat)] do |vars| 88 | vars[:context].state = Context::PRINT_RESULTS 89 | modify vars[:context] 90 | end 91 | 92 | rule :continue, {:priority => -5}, 93 | [Context,:context,m.state == Context::CHECK_DONE] do |vars| 94 | vars[:context].state = Context::ASSIGN_SEATS 95 | modify vars[:context] 96 | end 97 | 98 | rule :allDone, 99 | [Context,:context,m.state == Context::PRINT_RESULTS] do |vars| 100 | puts 'All done' 101 | end 102 | end 103 | end 104 | 105 | end -------------------------------------------------------------------------------- /benchmarks/miss_manners/data.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | module MissManners 13 | class MannersData 14 | def initialize 15 | @guests16 = [ 16 | Guest.new(:n1, :f, :h3), 17 | Guest.new(:n1, :f, :h1), 18 | Guest.new(:n1, :f, :h2), 19 | Guest.new(:n2, :f, :h3), 20 | Guest.new(:n2, :f, :h2), 21 | Guest.new(:n3, :m, :h1), 22 | Guest.new(:n3, :m, :h3), 23 | Guest.new(:n4, :m, :h2), 24 | Guest.new(:n4, :m, :h1), 25 | Guest.new(:n5, :m, :h2), 26 | Guest.new(:n5, :m, :h3), 27 | Guest.new(:n6, :m, :h2), 28 | Guest.new(:n6, :m, :h1), 29 | Guest.new(:n7, :f, :h2), 30 | Guest.new(:n7, :f, :h1), 31 | Guest.new(:n7, :f, :h3), 32 | Guest.new(:n8, :f, :h3), 33 | Guest.new(:n8, :f, :h2), 34 | Guest.new(:n9, :f, :h1), 35 | Guest.new(:n9, :f, :h3), 36 | Guest.new(:n9, :f, :h2), 37 | Guest.new(:n10, :m, :h2), 38 | Guest.new(:n10, :m, :h3), 39 | Guest.new(:n11, :m, :h3), 40 | Guest.new(:n11, :m, :h2), 41 | Guest.new(:n11, :m, :h1), 42 | Guest.new(:n12, :m, :h3), 43 | Guest.new(:n12, :m, :h1), 44 | Guest.new(:n13, :m, :h2), 45 | Guest.new(:n13, :m, :h3), 46 | Guest.new(:n13, :m, :h1), 47 | Guest.new(:n14, :f, :h3), 48 | Guest.new(:n14, :f, :h1), 49 | Guest.new(:n15, :f, :h3), 50 | Guest.new(:n15, :f, :h2), 51 | Guest.new(:n15, :f, :h1), 52 | Guest.new(:n16, :f, :h3), 53 | Guest.new(:n16, :f, :h2), 54 | Guest.new(:n16, :f, :h1), 55 | LastSeat.new(16)] 56 | 57 | @guests32 = [ 58 | Guest.new(:n1, :m, :h1), 59 | Guest.new(:n1, :m, :h3), 60 | Guest.new(:n2, :f, :h3), 61 | Guest.new(:n2, :f, :h2), 62 | Guest.new(:n2, :f, :h1), 63 | Guest.new(:n3, :f, :h1), 64 | Guest.new(:n3, :f, :h2), 65 | Guest.new(:n4, :f, :h3), 66 | Guest.new(:n4, :f, :h1), 67 | Guest.new(:n5, :f, :h1), 68 | Guest.new(:n5, :f, :h2), 69 | Guest.new(:n6, :m, :h1), 70 | Guest.new(:n6, :m, :h2), 71 | Guest.new(:n6, :m, :h3), 72 | Guest.new(:n7, :f, :h2), 73 | Guest.new(:n7, :f, :h1), 74 | Guest.new(:n7, :f, :h3), 75 | Guest.new(:n8, :f, :h1), 76 | Guest.new(:n8, :f, :h3), 77 | Guest.new(:n8, :f, :h2), 78 | Guest.new(:n9, :f, :h1), 79 | Guest.new(:n9, :f, :h3), 80 | Guest.new(:n9, :f, :h2), 81 | Guest.new(:n10, :m, :h2), 82 | Guest.new(:n10, :m, :h1), 83 | Guest.new(:n11, :m, :h2), 84 | Guest.new(:n11, :m, :h1), 85 | Guest.new(:n12, :m, :h3), 86 | Guest.new(:n12, :m, :h2), 87 | Guest.new(:n13, :m, :h1), 88 | Guest.new(:n13, :m, :h3), 89 | Guest.new(:n14, :m, :h3), 90 | Guest.new(:n14, :m, :h2), 91 | Guest.new(:n15, :f, :h2), 92 | Guest.new(:n15, :f, :h1), 93 | Guest.new(:n15, :f, :h3), 94 | Guest.new(:n16, :f, :h3), 95 | Guest.new(:n16, :f, :h2), 96 | Guest.new(:n16, :f, :h1), 97 | Guest.new(:n17, :m, :h3), 98 | Guest.new(:n17, :m, :h2), 99 | Guest.new(:n18, :f, :h2), 100 | Guest.new(:n18, :f, :h1), 101 | Guest.new(:n19, :f, :h1), 102 | Guest.new(:n19, :f, :h2), 103 | Guest.new(:n19, :f, :h3), 104 | Guest.new(:n20, :f, :h1), 105 | Guest.new(:n20, :f, :h2), 106 | Guest.new(:n20, :f, :h3), 107 | Guest.new(:n21, :m, :h2), 108 | Guest.new(:n21, :m, :h3), 109 | Guest.new(:n21, :m, :h1), 110 | Guest.new(:n22, :f, :h1), 111 | Guest.new(:n22, :f, :h2), 112 | Guest.new(:n22, :f, :h3), 113 | Guest.new(:n23, :f, :h3), 114 | Guest.new(:n23, :f, :h1), 115 | Guest.new(:n23, :f, :h2), 116 | Guest.new(:n24, :m, :h1), 117 | Guest.new(:n24, :m, :h3), 118 | Guest.new(:n25, :f, :h3), 119 | Guest.new(:n25, :f, :h2), 120 | Guest.new(:n25, :f, :h1), 121 | Guest.new(:n26, :f, :h3), 122 | Guest.new(:n26, :f, :h2), 123 | Guest.new(:n26, :f, :h1), 124 | Guest.new(:n27, :m, :h3), 125 | Guest.new(:n27, :m, :h1), 126 | Guest.new(:n27, :m, :h2), 127 | Guest.new(:n28, :m, :h3), 128 | Guest.new(:n28, :m, :h1), 129 | Guest.new(:n29, :m, :h3), 130 | Guest.new(:n29, :m, :h2), 131 | Guest.new(:n29, :m, :h1), 132 | Guest.new(:n30, :m, :h2), 133 | Guest.new(:n30, :m, :h1), 134 | Guest.new(:n30, :m, :h3), 135 | Guest.new(:n31, :m, :h2), 136 | Guest.new(:n31, :m, :h1), 137 | Guest.new(:n32, :m, :h1), 138 | Guest.new(:n32, :m, :h3), 139 | Guest.new(:n32, :m, :h2), 140 | LastSeat.new(32)] 141 | end 142 | 143 | attr_reader :guests16 144 | attr_reader :guests32 145 | end 146 | end -------------------------------------------------------------------------------- /lib/core/atoms.rb: -------------------------------------------------------------------------------- 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 | #tokens 13 | module Ruleby 14 | module Core 15 | 16 | class Atom 17 | attr_reader :tag, :proc, :slot, :template 18 | 19 | def initialize(tag, slot, template, block) 20 | @tag = tag 21 | @slot = slot 22 | @template = template 23 | @proc = block 24 | end 25 | 26 | def to_s 27 | "#{self.class},#{@tag},#{@slot},#{@template}" 28 | end 29 | end 30 | 31 | # This kind of atom is used to match a simple condition. 32 | # For example: 33 | # 34 | # a.person{ |p| p.is_a? Person } 35 | # 36 | # So there are no references to other atoms. 37 | class PropertyAtom < Atom 38 | 39 | attr_reader :value 40 | 41 | def initialize(tag, slot, template, value, block) 42 | super(tag,slot,template, block) 43 | @value = value 44 | end 45 | 46 | def ==(atom) 47 | shareable?(atom) && @tag == atom.tag 48 | end 49 | 50 | def shareable?(atom) 51 | PropertyAtom === atom && 52 | @slot == atom.slot && 53 | @template == atom.template && 54 | @proc == atom.proc && 55 | @value == atom.value 56 | end 57 | end 58 | 59 | class FunctionAtom < Atom 60 | 61 | attr_reader :arguments 62 | 63 | def initialize(tag, template, arguments, block) 64 | @tag = tag 65 | @slot = nil 66 | @template = template 67 | @arguments = arguments 68 | @proc = block 69 | end 70 | 71 | def shareable?(atom) 72 | FunctionAtom === atom && 73 | @template == atom.template && 74 | @arguments == atom.arguments && 75 | @proc == atom.proc 76 | end 77 | 78 | def to_s 79 | "#{self.class},#{@template},#{@arguments.inspect}" 80 | end 81 | end 82 | 83 | # This kind of atom is used to match just a single, hard coded value. 84 | # For example: 85 | # 86 | # a.name == 'John' 87 | # 88 | # So there are no references to other atoms. 89 | class EqualsAtom < PropertyAtom 90 | EQUAL_PROC = lambda {|x, y| x == y} 91 | 92 | def initialize(tag, slot, template, value) 93 | super(tag,slot,template, value, EQUAL_PROC) 94 | end 95 | 96 | def shareable?(atom) 97 | EqualsAtom === atom && 98 | @slot == atom.slot && 99 | @template == atom.template 100 | end 101 | end 102 | 103 | # This kind of atom is used to match a class type. For example: 104 | # 105 | # 'For each Person as :p' 106 | # 107 | # It is only used at the start of a pattern. 108 | class HeadAtom < PropertyAtom 109 | HEAD_EQUAL_PROC = lambda {|t, c| t == c} 110 | HEAD_INHERITS_PROC = lambda {|t, c| t === c} 111 | 112 | def initialize(tag, template) 113 | if template.mode == :equals 114 | super tag, :class, template, template.clazz, HEAD_EQUAL_PROC 115 | elsif template.mode == :inherits 116 | super tag, :class, template, template.clazz, HEAD_INHERITS_PROC 117 | end 118 | end 119 | 120 | def shareable?(atom) 121 | HeadAtom === atom && @template == atom.template 122 | end 123 | end 124 | 125 | # This kind of atom is used for matching a value that is a variable. 126 | # For example: 127 | # 128 | # #name == #:your_name 129 | # 130 | # The expression for this atom depends on some other atom. 131 | class ReferenceAtom < Atom 132 | attr_reader :vars 133 | 134 | def initialize(tag, slot, vars, template, block) 135 | super(tag, slot, template, block) 136 | @vars = vars # list of referenced variable names 137 | end 138 | 139 | def shareable?(atom) 140 | false 141 | end 142 | 143 | def ==(atom) 144 | ReferenceAtom === atom && 145 | @proc == atom.proc && 146 | @tag == atom.tag && 147 | @vars == atom.vars && 148 | @template == atom.template 149 | end 150 | 151 | def to_s 152 | super + ", vars=#{vars.join(',')}" 153 | end 154 | end 155 | 156 | # This is an atom that references another atom that is in the same pattern. 157 | # Note that in a SelfReferenceAtom, the 'vars' argument must be a list of the 158 | # *slots* that this atom references (not the variable names)! 159 | class SelfReferenceAtom < ReferenceAtom 160 | end 161 | 162 | # This class encapsulates the criteria the HeadAtom uses to match. The clazz 163 | # attribute represents a Class type, and the mode defines whether the head 164 | # will match only class that are exactly a particular type, or if it will 165 | # match classes that inherit that type also. 166 | class Template 167 | attr_reader :clazz 168 | attr_reader :mode 169 | 170 | def initialize(clazz,mode=:equals) 171 | @clazz = clazz 172 | @mode = mode 173 | end 174 | 175 | def ==(df) 176 | Template === df && df.clazz == @clazz && df.mode == @mode 177 | end 178 | end 179 | 180 | class AtomFactory 181 | # TODO add some convenience methods for creating atoms 182 | end 183 | 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /spec/function_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class FuncFact 4 | attr :value, true 5 | attr :times, true 6 | def initialize(v=nil); @value = v; @times = 0; end 7 | end 8 | 9 | include Ruleby 10 | 11 | class FunctionsRulebook < Rulebook 12 | def rules_with_simple_function 13 | rule [FuncFact, :a, f("b", c{|a, b| b == "b"})] do |v| 14 | assert Success.new 15 | end 16 | end 17 | 18 | def rules_with_function_testing_self(arg) 19 | rule [FuncFact, :a, f(arg, c{|a, b| a.value == b})] do |v| 20 | assert Success.new 21 | end 22 | end 23 | 24 | def rules_that_share_a_function 25 | func = c{|a, b| a.times += 1; b == "foobar"} 26 | 27 | rule [FuncFact, :a, m.value > 1, f("foobar", func)] do |v| 28 | assert Success.new 29 | end 30 | 31 | rule [FuncFact, :a, m.value > 2, f("foobar", func)] do |v| 32 | assert Success.new 33 | end 34 | end 35 | 36 | def rules_with_many_args_function 37 | rule [FuncFact, :a, f([1, 2, 3, 4], c{|a, b, c, d, e| b < e})] do |v| 38 | assert Success.new 39 | end 40 | end 41 | 42 | def rules_with_no_args_function 43 | rule [FuncFact, :a, f(c{|a| !a.nil?})] do |v| 44 | assert Success.new 45 | end 46 | end 47 | end 48 | 49 | describe Ruleby::Rulebook do 50 | 51 | describe "#f" do 52 | context "rules_with_simple_function" do 53 | subject do 54 | engine :engine do |e| 55 | FunctionsRulebook.new(e).rules_with_simple_function 56 | end 57 | end 58 | 59 | context "with one FuncFact" do 60 | before do 61 | subject.assert FuncFact.new 62 | subject.match 63 | end 64 | 65 | it "should match once" do 66 | r = subject.retrieve Success 67 | r.size.should == 1 68 | subject.errors.should == [] 69 | end 70 | end 71 | end 72 | 73 | context "rules_with_function_testing_self(:foo)" do 74 | subject do 75 | engine :engine do |e| 76 | FunctionsRulebook.new(e).rules_with_function_testing_self(:foo) 77 | end 78 | end 79 | 80 | context "with one FuncFact" do 81 | before do 82 | subject.assert FuncFact.new(:foo) 83 | subject.match 84 | end 85 | 86 | it "should match once" do 87 | r = subject.retrieve Success 88 | r.size.should == 1 89 | subject.errors.should == [] 90 | end 91 | end 92 | end 93 | 94 | context "rules_with_function_testing_self(:bar)" do 95 | subject do 96 | engine :engine do |e| 97 | FunctionsRulebook.new(e).rules_with_function_testing_self(:bar) 98 | end 99 | end 100 | 101 | context "with one FuncFact" do 102 | before do 103 | subject.assert FuncFact.new(:foo) 104 | subject.match 105 | end 106 | 107 | it "should not match " do 108 | r = subject.retrieve Success 109 | r.size.should == 0 110 | subject.errors.should == [] 111 | end 112 | end 113 | end 114 | 115 | context "rules_that_share_a_function" do 116 | subject do 117 | engine :engine do |e| 118 | FunctionsRulebook.new(e).rules_that_share_a_function 119 | end 120 | end 121 | 122 | context "with one FuncFact" do 123 | before do 124 | @f = FuncFact.new(3) 125 | subject.assert @f 126 | subject.match 127 | end 128 | 129 | it "should match, node should be shared, function should be evaled once" do 130 | r = subject.retrieve Success 131 | r.size.should == 2 132 | subject.errors.should == [] 133 | 134 | @f.times.should == 1 135 | 136 | subject.retract r[0] 137 | subject.retract r[1] 138 | subject.retract @f 139 | subject.match 140 | 141 | r = subject.retrieve Success 142 | r.size.should == 0 143 | subject.errors.should == [] 144 | 145 | subject.assert @f 146 | subject.match 147 | 148 | r = subject.retrieve Success 149 | r.size.should == 2 150 | subject.errors.should == [] 151 | 152 | @f.times.should == 2 153 | end 154 | end 155 | end 156 | 157 | context "rules_with_many_args_function" do 158 | subject do 159 | engine :engine do |e| 160 | FunctionsRulebook.new(e).rules_with_many_args_function 161 | end 162 | end 163 | 164 | context "with one FuncFact" do 165 | before do 166 | subject.assert FuncFact.new 167 | subject.match 168 | end 169 | 170 | it "should match once" do 171 | r = subject.retrieve Success 172 | r.size.should == 1 173 | subject.errors.should == [] 174 | end 175 | end 176 | end 177 | 178 | context "rules_with_no_args_function" do 179 | subject do 180 | engine :engine do |e| 181 | FunctionsRulebook.new(e).rules_with_no_args_function 182 | end 183 | end 184 | 185 | context "with one FuncFact" do 186 | before do 187 | subject.assert FuncFact.new 188 | subject.match 189 | end 190 | 191 | it "should match once" do 192 | r = subject.retrieve Success 193 | r.size.should == 1 194 | subject.errors.should == [] 195 | end 196 | end 197 | end 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /benchmarks/miss_manners/model.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | module MissManners 13 | 14 | class Chosen 15 | def initialize(id,guestName,hobby) 16 | @id = id 17 | @guestName = guestName 18 | @hobby = hobby 19 | end 20 | attr_reader :id, :guestName, :hobby 21 | def to_s 22 | "{Chosen id=#{@id}, name=#{@guestName}, hobbies=#{@hobby}}" 23 | end 24 | end 25 | 26 | class Context 27 | START_UP = 0 28 | ASSIGN_SEATS = 1 29 | MAKE_PATH = 2 30 | CHECK_DONE = 3 31 | PRINT_RESULTS = 4 32 | STATE_STRINGS = ["START_UP","ASSIGN_SEATS","MAKE_PATH","CHECK_DONE","PRINT_RESULTS"] 33 | def initialize(state) 34 | if state == :start 35 | @state = START_UP 36 | else 37 | raise "Context #{state.to_s} does not exist for Context Enum" 38 | end 39 | end 40 | attr :state, true 41 | def string_value 42 | return STATE_STRINGS[@state] 43 | end 44 | def is_state(state) 45 | return @state == state 46 | end 47 | def to_s 48 | return "[Context state=" + string_value.to_s + "]"; 49 | end 50 | end 51 | 52 | class Count 53 | def initialize(value) 54 | @value = value 55 | end 56 | attr :value, true 57 | def ==(object) 58 | if object.object_id == self.object_id 59 | return true 60 | elsif object == nil || !(object.kind_of?(Count)) 61 | return false 62 | end 63 | return @value == object.value; 64 | end 65 | def to_s 66 | return "[Count value=#{@value.to_s}]" 67 | end 68 | def to_hash 69 | return value.to_hash 70 | end 71 | end 72 | 73 | class Guest 74 | def initialize(name,sex,hobby) 75 | @name = name 76 | @sex = sex 77 | @hobby = hobby 78 | end 79 | attr_reader :name,:sex,:hobby 80 | def to_s 81 | return "[Guest name=" + @name.to_s + ", sex=" + @sex.to_s + ", hobbies=" + @hobby.to_s + "]"; 82 | end 83 | end 84 | 85 | class Hobby 86 | @stringH1 = "h1" 87 | @stringH2 = "h2" 88 | @stringH3 = "h3" 89 | @stringH4 = "h4" 90 | @stringH5 = "h5" 91 | HOBBY_STRINGS = [@stringH1,@stringH2,@stringH3,@stringH4,@stringH5] 92 | def initialize(hobby) 93 | @hobbyIndex = hobby-1 94 | @hobby = HOBBY_STRINGS[@hobbyIndex] 95 | end 96 | H1 = Hobby.new(1) 97 | H2 = Hobby.new(2) 98 | H3 = Hobby.new(3) 99 | H4 = Hobby.new(4) 100 | H5 = Hobby.new(5) 101 | attr_reader :hobby 102 | def resolve(hobby) 103 | if @stringH1 == hobby 104 | return H1 105 | elsif @stringH2 == hobby 106 | return H2 107 | elsif @stringH3 == hobby 108 | return H3 109 | elsif @stringH4 == hobby 110 | return H4 111 | elsif @stringH5 == hobby 112 | return H5 113 | else 114 | raise "Hobby '" + @hobby.to_s + "' does not exist for Hobby Enum" 115 | end 116 | end 117 | def to_hash 118 | return @hobbyIndex.to_hash 119 | end 120 | def to_s 121 | return @hobby 122 | end 123 | end 124 | 125 | class LastSeat 126 | def initialize(seat) 127 | @seat = seat 128 | end 129 | attr_reader :seat 130 | def to_s 131 | return "[LastSeat seat=#{@seat.to_s}]" 132 | end 133 | end 134 | 135 | class Path 136 | def initialize(id,seat,guestName) 137 | @id = id 138 | @guestName = guestName 139 | @seat = seat 140 | end 141 | attr_reader :id, :guestName, :seat 142 | def to_s 143 | "[Path id=#{@id.to_s}, name=#{@guestName.to_s}, seat=#{@seat.to_s}]" 144 | end 145 | end 146 | 147 | class Seating 148 | def initialize(id,pid,path,leftSeat,leftGuestName,rightSeat,rightGuestName) 149 | @id = id 150 | @pid = pid 151 | @path = path 152 | @leftSeat = leftSeat 153 | @leftGuestName = leftGuestName 154 | @rightSeat = rightSeat 155 | @rightGuestName = rightGuestName 156 | end 157 | attr :path, true 158 | attr_reader :id,:pid,:leftSeat,:leftGuestName,:rightSeat,:rightGuestName 159 | def to_s 160 | return "[Seating id=#{@id.to_s} , pid=#{pid.to_s} , pathDone=#{@path.to_s} , leftSeat=#{@leftSeat.to_s}, leftGuestName=#{@leftGuestName.to_s}, rightSeat=#{@rightSeat.to_s}, rightGuestName=#{@rightGuestName.to_s}]"; 161 | end 162 | end 163 | 164 | class Sex 165 | STRING_M = 'm' 166 | STRING_F = 'f' 167 | SEX_LIST = [ STRING_M, STRING_F ] 168 | def initialize(sex) 169 | @sex = sex 170 | end 171 | M = Sex.new( 0 ) 172 | F = Sex.new( 1 ) 173 | def sex 174 | return SEX_LIST[sex] 175 | end 176 | def resolve(sex) 177 | if STRING_M == sex 178 | return M 179 | elsif STRING_F == sex 180 | return F 181 | else 182 | raise "Sex '#{@sex.to_s}' does not exist for Sex Enum" 183 | end 184 | end 185 | def to_s 186 | return @sex.to_s 187 | end 188 | def to_hash 189 | return @sex.to_hash 190 | end 191 | end 192 | 193 | end -------------------------------------------------------------------------------- /tests/or_patterns.rb: -------------------------------------------------------------------------------- 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) 2009 Joe Kutner and Matt Smith. All rights reserved. 8 | # 9 | # * Authors: Joe Kutner 10 | # 11 | 12 | require 'test/unit' 13 | 14 | require 'ruleby' 15 | 16 | include Ruleby 17 | 18 | module OrPatterns 19 | 20 | class OrPatternsRulebook < Rulebook 21 | def rules 22 | rule OR([Message, m.message == :FIRST], [Message, m.message == :SECOND]), [Context, :c] do |v| 23 | v[:c].inc :rule1 24 | end 25 | 26 | rule OR([Message, m.message == :FIRST], [Message, m.message == :FOOBAR]), [Context, :c] do |v| 27 | v[:c].inc :rule2 28 | end 29 | 30 | rule OR([Message, m.message == :FIRST], [Message, m.message == :FIRST]), [Context, :c] do |v| 31 | v[:c].inc :rule3 32 | end 33 | 34 | rule OR([Message, m.message == :FOO], [Message, m.message == :BAR]), [Context, :c] do |v| 35 | # this is not expected to pass 36 | v[:c].inc :rule4 37 | end 38 | 39 | rule OR([Message, m.message == :FOOBAR], [Message, m.message == :SECOND]), [Context, :c] do |v| 40 | v[:c].inc :rule5 41 | end 42 | 43 | rule AND([Message, m.message == :FIRST], [Message, m.message == :SECOND]), [Context, :c] do |v| 44 | # no reason for this to work - its verbose, just checking. 45 | v[:c].inc :rule6 46 | end 47 | 48 | rule AND([Message, m.message == :FOOBAR], [Message, m.message == :SECOND]), [Context, :c] do |v| 49 | # no reason for this to work - its verbose, just checking. 50 | v[:c].inc :rule7 51 | end 52 | 53 | rule OR(AND([Message, m.message == :FIRST], [Message, m.message == :SECOND])), [Context, :c] do |v| 54 | v[:c].inc :rule8 55 | end 56 | 57 | rule OR([Message, m.message == :FIRST]), [Context, :c] do |v| 58 | v[:c].inc :rule9 59 | end 60 | 61 | rule OR(AND([Message, m.message == :FIRST], [Message, m.message == :SECOND]), [Message, m.message == :FOOBAR]), [Context, :c] do |v| 62 | v[:c].inc :rule10 63 | end 64 | 65 | rule AND([Message, m.message == :FIRST], [Context, :c]) do |v| 66 | # no reason for this to work - its verbose. But it does work, so just checking. 67 | v[:c].inc :rule11 68 | end 69 | 70 | rule AND([Message, m.message == :FOOBAR], [Context, :c]) do |v| 71 | # no reason for this to work - its verbose. But it does work, so just checking. 72 | v[:c].inc :rule12 73 | end 74 | 75 | rule OR([Message, :f, m.message == :FOOBAR], [Message, :g, m.message == :SECOND]), [Context, :c] do |v| 76 | if v[:f] 77 | # :f should be null 78 | v[:c].inc :rule13a 79 | end 80 | 81 | 82 | if v[:g] 83 | # :g should never be null 84 | v[:c].inc :rule13b 85 | end 86 | end 87 | 88 | rule OR([Message, :f, m.message == :FIRST], [Message, :s, m.message == :SECOND]), [Context, :c] do |v| 89 | if v[:f] 90 | v[:c].inc :rule14a 91 | end 92 | 93 | if v[:s] 94 | v[:c].inc :rule14b 95 | end 96 | end 97 | 98 | rule OR(AND([Message, m.message == :FIRST], [Message, m.message == :SECOND]), [Message, m.message == :FOOBAR], [Message, m.message == :THIRD]), [Context, :c] do |v| 99 | v[:c].inc :rule16 100 | end 101 | 102 | rule OR(AND(OR([Message, m.message == :FIRST], [Message, m.message == :SECOND]), [Message, m.message == :THIRD])), [Context, :c] do |v| 103 | v[:c].inc :rule17 104 | end 105 | 106 | rule OR(AND(OR(OR([Message, m.message == :FIRST])))), [Context, :c] do |v| 107 | v[:c].inc :rule18 108 | end 109 | 110 | rule OR(AND(OR(OR([Message, m.message == :FOOBAR])))), [Context, :c] do |v| 111 | v[:c].inc :rule19 112 | end 113 | 114 | rule OR([Message, m.message == :FIRST], [Message, m.message == :SECOND], [Message, m.message == :THIRD]), [Context, :c] do |v| 115 | v[:c].inc :rule20 116 | end 117 | end 118 | end 119 | 120 | class Test < Test::Unit::TestCase 121 | def test_0 122 | engine :engine do |e| 123 | OrPatternsRulebook.new(e).rules 124 | ctx = Context.new 125 | e.assert ctx 126 | e.assert Message.new(:FIRST, :FIRST) 127 | e.assert Message.new(:FIRST, :SECOND) 128 | e.assert Message.new(:FIRST, :THIRD) 129 | e.match 130 | assert_equal 3, ctx.get(:rule20) 131 | assert_equal 0, ctx.get(:rule19) 132 | assert_equal 1, ctx.get(:rule18) 133 | assert_equal 2, ctx.get(:rule17) 134 | assert_equal 2, ctx.get(:rule16) 135 | assert_equal 1, ctx.get(:rule14b) 136 | assert_equal 1, ctx.get(:rule14a) 137 | assert_equal 1, ctx.get(:rule13b) 138 | assert_equal 0, ctx.get(:rule13a) 139 | assert_equal 0, ctx.get(:rule12) 140 | assert_equal 1, ctx.get(:rule11) 141 | assert_equal 1, ctx.get(:rule10) 142 | assert_equal 1, ctx.get(:rule9) 143 | assert_equal 1, ctx.get(:rule8) 144 | assert_equal 0, ctx.get(:rule7) 145 | assert_equal 1, ctx.get(:rule6) 146 | assert_equal 1, ctx.get(:rule5) 147 | assert_equal 0, ctx.get(:rule4) 148 | assert_equal 2, ctx.get(:rule3) 149 | assert_equal 1, ctx.get(:rule2) 150 | assert_equal 2, ctx.get(:rule1) 151 | end 152 | end 153 | end 154 | end -------------------------------------------------------------------------------- /spec/and_or_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class AndOrFact 4 | attr :value, true 5 | def initialize(v=nil); @value = v; end 6 | end 7 | 8 | class AndOrFact2 9 | attr :value, true 10 | def initialize(v=nil); @value = v; end 11 | end 12 | 13 | class AndOrFact3 14 | attr :value, true 15 | def initialize(v=nil); @value = v; end 16 | end 17 | 18 | class AndOrFact4 19 | attr :value, true 20 | def initialize(v=nil); @value = v; end 21 | end 22 | 23 | class AndOrFact5 24 | attr :value, true 25 | def initialize(v=nil); @value = v; end 26 | end 27 | 28 | class AndOrFact6 29 | attr :value, true 30 | def initialize(v=nil); @value = v; end 31 | end 32 | 33 | include Ruleby 34 | 35 | class AndOrRulebook < Rulebook 36 | def rules 37 | rule AND( 38 | OR([AndOrFact, m.value > 0]), 39 | OR( 40 | OR([AndOrFact, m.value == 1]), 41 | AND( 42 | AND([AndOrFact, m.value < 1]), 43 | OR([AndOrFact, m.value == nil], [:not, AndOrFact])))) do 44 | assert Success.new 45 | end 46 | 47 | # rule [AndOrFact, m.value > 0], 48 | # OR( 49 | # [AndOrFact, m.value == 1], 50 | # AND( 51 | # [AndOrFact, m.value < 1], 52 | # OR([AndOrFact, m.value == nil], [:not, AndOrFact]))) do 53 | # assert Success.new 54 | # end 55 | end 56 | 57 | def rules2 58 | rule OR(AND(OR(OR([AndOrFact, m.value == 1])))) do |v| 59 | assert Success.new 60 | end 61 | end 62 | 63 | def rules3 64 | rule OR([AndOrFact, m.value == 1], [AndOrFact, m.value == 2], [AndOrFact, m.value == 3]), [AndOrFact, m.value == 4] do |v| 65 | assert Success.new 66 | end 67 | end 68 | 69 | def rules4 70 | rule AND([AndOrFact, :a, m.value == 1], [AndOrFact2, :a2, m.value == 2]) do |v| 71 | raise "nil" if v[:a].nil? 72 | raise "nil" if v[:a2].nil? 73 | assert Success.new 74 | end 75 | end 76 | 77 | def rules5 78 | rule OR(AND([AndOrFact, :a, {m.value == 1 => :x}], [AndOrFact2, m.value == b(:x)])) do |v| 79 | assert Success.new 80 | end 81 | end 82 | 83 | def rules6 84 | rule OR( 85 | AND([:collect, AndOrFact, m.value == 1]), 86 | AND([:collect, AndOrFact2, m.value == 2]) 87 | ), AND( 88 | AND([AndOrFact3, m.value == 65]), 89 | OR( 90 | OR( 91 | OR([AndOrFact4, m.value == 4]), 92 | AND([AndOrFact5, m.value == 5]) 93 | ), 94 | OR([AndOrFact6, m.value == 6]) 95 | ) 96 | ) do |v| 97 | assert Success.new 98 | end 99 | # all that really matters 100 | # rule OR( 101 | # [AndOrFact, m.value == 1], 102 | # [AndOrFact2, m.value == 2] 103 | # ), AND( 104 | # [AndOrFact3, m.value == 65], 105 | # OR( 106 | # [AndOrFact4, m.value == 4], 107 | # [AndOrFact5, m.value == 5] 108 | # ) 109 | # ) do |v| 110 | # assert Success.new 111 | # end 112 | end 113 | end 114 | 115 | describe Ruleby::Core::Engine do 116 | # describe "AND/OR" do 117 | context "crazy AND/OR rules" do 118 | subject do 119 | engine :engine do |e| 120 | AndOrRulebook.new(e).rules 121 | end 122 | end 123 | 124 | before do 125 | subject.assert AndOrFact.new(1) 126 | subject.match 127 | end 128 | 129 | it "should have matched" do 130 | subject.errors.should == [] 131 | subject.retrieve(Success).size.should == 1 132 | end 133 | end 134 | 135 | context "nested AND/OR rule" do 136 | subject do 137 | engine :engine do |e| 138 | AndOrRulebook.new(e).rules2 139 | end 140 | end 141 | 142 | before do 143 | subject.assert AndOrFact.new(1) 144 | subject.match 145 | end 146 | 147 | it "should have matched" do 148 | subject.errors.should == [] 149 | subject.retrieve(Success).size.should == 1 150 | end 151 | end 152 | 153 | context "multi OR rule" do 154 | subject do 155 | engine :engine do |e| 156 | AndOrRulebook.new(e).rules3 157 | end 158 | end 159 | 160 | context "with one 1 and one 4" do 161 | before do 162 | subject.assert AndOrFact.new(1) 163 | subject.assert AndOrFact.new(4) 164 | subject.match 165 | end 166 | 167 | it "should have matched" do 168 | subject.errors.should == [] 169 | subject.retrieve(Success).size.should == 1 170 | end 171 | end 172 | end 173 | 174 | context "nested AND/OR rule" do 175 | subject do 176 | engine :engine do |e| 177 | AndOrRulebook.new(e).rules4 178 | end 179 | end 180 | 181 | before do 182 | subject.assert AndOrFact.new(1) 183 | subject.assert AndOrFact2.new(2) 184 | subject.match 185 | end 186 | 187 | it "should have matched" do 188 | subject.errors.should == [] 189 | subject.retrieve(Success).size.should == 1 190 | end 191 | end 192 | 193 | context "nested AND/OR rule" do 194 | subject do 195 | engine :engine do |e| 196 | AndOrRulebook.new(e).rules5 197 | end 198 | end 199 | 200 | before do 201 | subject.assert AndOrFact.new(1) 202 | subject.assert AndOrFact2.new(1) 203 | subject.match 204 | end 205 | 206 | it "should have matched" do 207 | subject.errors.should == [] 208 | subject.retrieve(Success).size.should == 1 209 | end 210 | end 211 | 212 | context "another crazy nested AND/OR rule" do 213 | subject do 214 | engine :engine do |e| 215 | AndOrRulebook.new(e).rules6 216 | end 217 | end 218 | 219 | before do 220 | subject.assert AndOrFact.new(1) 221 | subject.assert AndOrFact3.new(3) 222 | subject.assert AndOrFact4.new(4) 223 | subject.match 224 | end 225 | 226 | it "should not have matched" do 227 | subject.errors.should == [] 228 | subject.retrieve(Success).size.should == 0 229 | end 230 | end 231 | 232 | context "another crazy nested AND/OR rule" do 233 | subject do 234 | engine :engine do |e| 235 | AndOrRulebook.new(e).rules6 236 | end 237 | end 238 | 239 | before do 240 | subject.assert AndOrFact.new(1) 241 | subject.assert AndOrFact3.new(65) 242 | subject.assert AndOrFact4.new(4) 243 | subject.match 244 | end 245 | 246 | it "should have matched" do 247 | subject.errors.should == [] 248 | subject.retrieve(Success).size.should == 1 249 | end 250 | end 251 | # end 252 | end -------------------------------------------------------------------------------- /lib/core/engine.rb: -------------------------------------------------------------------------------- 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, Matt Smith 10 | # 11 | 12 | require 'core/atoms' 13 | require 'core/patterns' 14 | require 'core/utils' 15 | require 'core/nodes' 16 | 17 | module Ruleby 18 | module Core 19 | 20 | # An action is a wrapper for a code block that will be executed if a rule is 21 | # satisfied. 22 | class Action 23 | attr_accessor :priority 24 | attr_accessor :name 25 | attr_reader :matches 26 | attr_reader :proc 27 | 28 | def initialize(&block) 29 | @name = nil 30 | @proc = Proc.new(&block) if block_given? 31 | @priority = 0 32 | end 33 | 34 | def fire(match, engine=nil) 35 | if @proc.arity == 2 36 | @proc.call(match, engine) 37 | else 38 | @proc.call(match) 39 | end 40 | end 41 | 42 | def ==(a2) 43 | return @name == a2.name 44 | end 45 | end 46 | 47 | # An activation is an action/match pair that is executed if a rule is matched. 48 | # It also contains metadata that can be used for conflict resolution if two 49 | # rules are satisfied by the same fact. 50 | class Activation 51 | attr_reader :action, :match 52 | attr_accessor :counter, :used 53 | 54 | def initialize(action, match, counter=0) 55 | @action = action 56 | @match = match 57 | @match.recency.sort! 58 | @match.recency.reverse! 59 | @counter = counter 60 | @used = false 61 | end 62 | 63 | def fire(engine=nil) 64 | @used = true 65 | @action.fire @match, engine 66 | end 67 | 68 | def <=>(a2) 69 | return @counter <=> a2.counter if @counter != a2.counter 70 | return @action.priority <=> a2.action.priority if @action.priority != a2.action.priority 71 | 72 | # NOTE in order for this to work, the array must be reverse sorted 73 | i = 0; while @match.recency[i] == a2.match.recency[i] && i < @match.recency.size-1 && i < a2.match.recency.size-1 74 | i += 1 75 | end 76 | @match.recency[i] <=> a2.match.recency[i] 77 | end 78 | 79 | def ==(a2) 80 | a2 != nil && @action == a2.action && @match == a2.match 81 | end 82 | 83 | def modify(match) 84 | @match = match 85 | # should we update recency, too? 86 | end 87 | 88 | def to_s 89 | return "[#{@action.name}-#{object_id}|#{@counter}|#{@action.priority}|#{@match.recency.join(',')}|#{@match.to_s}] " 90 | end 91 | end 92 | 93 | class Rule 94 | attr_accessor :pattern 95 | attr_reader :action, :name, :priority 96 | 97 | def initialize(name, pattern=nil, action=nil, priority=0) 98 | @name = name 99 | @pattern = pattern 100 | @action = action 101 | @priority = priority 102 | end 103 | 104 | def priority=(p) 105 | @priority = p 106 | @action.priority = @priority 107 | end 108 | end 109 | 110 | # A fact is an object that is stored in working memory. The rules in the 111 | # system will either look for the existence or absence of particular facts. 112 | class Fact 113 | attr :recency, true 114 | attr_reader :object 115 | 116 | def initialize(object) 117 | @object = object 118 | end 119 | 120 | def id 121 | return object.object_id 122 | end 123 | 124 | def ==(fact) 125 | if fact.is_a? Fact 126 | fact != nil && fact.id == id 127 | else 128 | fact != nil && fact.object_id == id 129 | end 130 | end 131 | 132 | def to_s 133 | "[Fact |#{@recency}|#{@object.to_s}]" 134 | end 135 | end 136 | 137 | # A conflict resolver is used to order activations that become active at the 138 | # same time. The default implementation sorts the agenda based on the 139 | # properties of the activation. 140 | class RulebyConflictResolver 141 | def resolve(agenda) 142 | return agenda.sort 143 | end 144 | end 145 | 146 | # The working memory is a container for all the facts in the system. The 147 | # inference engine will compare these facts with the rules to produce some 148 | # outcomes. 149 | class WorkingMemory 150 | attr_reader :facts 151 | 152 | def initialize 153 | @recency = 0 154 | @facts = Array.new 155 | end 156 | 157 | def each_fact 158 | @facts.each do |f| 159 | yield(f) 160 | end 161 | end 162 | 163 | def assert_fact(fact) 164 | raise 'The fact asserted cannot be nil!' if fact.object.nil? 165 | fact.recency = @recency 166 | @recency += 1 167 | @facts.push fact 168 | return fact 169 | end 170 | 171 | def retract_fact(fact) 172 | i = @facts.index(fact) 173 | raise 'The fact to remove does not exist!' unless i 174 | existing_fact = @facts[i] 175 | @facts.delete_at(i) 176 | return existing_fact 177 | end 178 | 179 | def print 180 | puts 'WORKING MEMORY:' 181 | @facts.each do |fact| 182 | puts " #{fact.object} - #{fact.id} - #{fact.recency}" 183 | end 184 | end 185 | end 186 | 187 | class Error 188 | attr_reader :type, :level, :details 189 | 190 | def initialize(type, level, details={}) 191 | @type = type 192 | @details = details 193 | @level = level 194 | end 195 | end 196 | 197 | # This is the core class of the library. A new rule engine is created by 198 | # instantiating it. Each rule engine has one inference engine, one rule set 199 | # and one working memory. 200 | class Engine 201 | 202 | def initialize(wm=WorkingMemory.new,cr=RulebyConflictResolver.new) 203 | @root = nil 204 | @working_memory = wm 205 | @conflict_resolver = cr 206 | @wm_altered = false 207 | assert InitialFact.new 208 | end 209 | 210 | def facts 211 | @working_memory.facts.collect{|f| f.object}.select{|f| !f.is_a?(InitialFact)} 212 | end 213 | 214 | # This method id called to add a new fact to working memory 215 | def assert(object,&block) 216 | @wm_altered = true 217 | fact_helper(object,:plus,&block) 218 | end 219 | 220 | # This method is called to remove an existing fact from working memory 221 | def retract(object,&block) 222 | @wm_altered = true 223 | fact_helper(object,:minus,&block) 224 | object 225 | end 226 | 227 | # This method is called to alter an existing fact. It is essentially a 228 | # retract followed by an assert. 229 | def modify(object,&block) 230 | retract(object,&block) 231 | assert(object,&block) 232 | end 233 | 234 | def retrieve(c) 235 | facts.select {|f| f.kind_of?(c)} 236 | end 237 | 238 | # This method adds a new rule to the system. 239 | def assert_rule(rule) 240 | if @root == nil 241 | @root = RootNode.new(@working_memory) 242 | @root.reset_counter 243 | end 244 | @root.assert_rule rule 245 | end 246 | 247 | # This method executes the activations that were generated by the rules 248 | # that match facts in working memory. 249 | def match(agenda=nil, used_agenda=[]) 250 | if @root 251 | @root.reset_counter 252 | agenda = @root.matches unless agenda 253 | while (agenda.length > 0) 254 | agenda = @conflict_resolver.resolve agenda 255 | activation = agenda.pop 256 | used_agenda.push activation 257 | activation.fire self 258 | if @wm_altered 259 | agenda = @root.matches(false) 260 | @root.increment_counter 261 | @wm_altered = false 262 | end 263 | end 264 | end 265 | end 266 | 267 | def errors 268 | @root.nil? ? [] : @root.errors 269 | end 270 | 271 | def clear_errors 272 | @root.clear_errors if @root 273 | end 274 | 275 | def print 276 | @working_memory.print 277 | @root.print 278 | end 279 | 280 | private 281 | def fact_helper(object, sign=:plus) 282 | f = Core::Fact.new object 283 | yield f if block_given? 284 | sign==:plus ? assert_fact(f) : retract_fact(f) 285 | f 286 | end 287 | 288 | def assert_fact(fact) 289 | wm_fact = @working_memory.assert_fact fact 290 | @root.assert_fact wm_fact if @root != nil 291 | end 292 | 293 | def retract_fact(fact) 294 | wm_fact = @working_memory.retract_fact fact 295 | @root.retract_fact wm_fact if @root != nil 296 | end 297 | end 298 | end 299 | end -------------------------------------------------------------------------------- /lib/dsl/ferrari.rb: -------------------------------------------------------------------------------- 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) 2010 Joe Kutner and Matt Smith. All rights reserved. 8 | # 9 | # * Authors: Joe Kutner, Matt Smith 10 | # 11 | 12 | module Ruleby 13 | module Ferrari 14 | class RulebookHelper 15 | def initialize(engine) 16 | @engine = engine 17 | end 18 | 19 | attr_reader :engine 20 | 21 | def rule(name, *args, &block) 22 | options = args[0].kind_of?(Hash) ? args.shift : {} 23 | 24 | rules = Ruleby::Ferrari.parse_containers(args, RulesContainer.new).build(name,options,@engine,&block) 25 | rules.each do |r| 26 | engine.assert_rule(r) 27 | end 28 | end 29 | end 30 | 31 | def self.parse_containers(args, container=Container(:and), parent=nil) 32 | con = nil 33 | if(container.kind_of?(RulesContainer)) 34 | con = Container.new(:and) 35 | else 36 | con = container 37 | end 38 | args.each do |arg| 39 | if arg.kind_of? Array 40 | con << PatternContainer.new(arg) 41 | elsif arg.kind_of? AndBuilder 42 | con << parse_containers(arg.conditions, Container.new(:and), container) 43 | elsif arg.kind_of? OrBuilder 44 | con << parse_containers(arg.conditions, Container.new(:or), container) 45 | else 46 | raise 'Invalid condition. Must be an OR, AND or an Array.' 47 | end 48 | end 49 | if container.kind_of?(RulesContainer) 50 | container << con 51 | end 52 | return container 53 | end 54 | 55 | class RulesContainer < Array 56 | def handle_branching 57 | ands = [] 58 | each do |x| 59 | f = x.flatten_patterns 60 | if f.or? 61 | f.each do |o| 62 | ands << o 63 | end 64 | else 65 | ands << f 66 | end 67 | end 68 | ands 69 | end 70 | 71 | def build(name, options, engine, &block) 72 | handle_branching.map do |container| 73 | build_rule(name, container, options, &block) 74 | end 75 | end 76 | 77 | def build_rule(name, container, options, &block) 78 | r = RuleBuilder.new name 79 | container.build r 80 | r.then(&block) 81 | r.priority = options[:priority] if options[:priority] 82 | r.build_rule 83 | end 84 | end 85 | 86 | 87 | class Container < Array 88 | attr_accessor :kind 89 | 90 | def initialize(kind, *vals) 91 | @kind = kind 92 | self.push(*vals) 93 | end 94 | 95 | def flatten_patterns 96 | if or? 97 | patterns = [] 98 | each do |c| 99 | f = c.flatten_patterns 100 | if f.and? 101 | patterns << f 102 | else 103 | f.each do |o| 104 | # i hope this is safe... not entirely sure 105 | patterns << (o.size == 1 ? o.first : o) 106 | # patterns << o 107 | end 108 | end 109 | end 110 | 111 | Container.new(:or, *patterns) 112 | elsif and? 113 | patterns = [] 114 | or_patterns = [] 115 | each do |c| 116 | child_patterns = c.flatten_patterns 117 | if child_patterns.or? and child_patterns.size > 1 118 | or_patterns << child_patterns 119 | else 120 | patterns.push(*child_patterns) 121 | end 122 | end 123 | if or_patterns.empty? 124 | flat = Container.new(:and) 125 | flat.push(*patterns) 126 | else 127 | flat = Container.new(:or) 128 | 129 | x = or_patterns[1..-1] 130 | if x.empty? 131 | or_pattern_products = or_patterns[0].product() 132 | else 133 | or_pattern_products = or_patterns[0].product(*x) 134 | end 135 | 136 | or_pattern_products.each do |op| 137 | c = Container.new(:and) 138 | c.push(*patterns) 139 | c.push(*op) 140 | flat << c 141 | end 142 | end 143 | return flat 144 | end 145 | end 146 | 147 | def build(builder) 148 | if self.or? 149 | # OrContainers are never built, they just contain containers that 150 | # will be transformed into AndContainers. 151 | raise 'Invalid Syntax' 152 | end 153 | self.each do |x| 154 | x.build builder 155 | end 156 | end 157 | 158 | def or? 159 | return kind == :or 160 | end 161 | 162 | def and? 163 | return kind == :and 164 | end 165 | end 166 | 167 | class PatternContainer 168 | def initialize(condition) 169 | @condition = condition 170 | end 171 | 172 | def size 173 | 1 174 | end 175 | 176 | def first 177 | self 178 | end 179 | 180 | def flatten_patterns 181 | Container.new(:and, self) 182 | end 183 | 184 | def build(builder) 185 | builder.when(*@condition) 186 | end 187 | 188 | def process_tree 189 | # there is no tree to process 190 | false 191 | end 192 | 193 | def or? 194 | false 195 | end 196 | 197 | def and? 198 | false 199 | end 200 | end 201 | 202 | class RuleBuilder 203 | 204 | def initialize(name, pattern=nil, action=nil, priority=0) 205 | @name = name 206 | @pattern = pattern 207 | @action = action 208 | @priority = priority 209 | 210 | @tags = {} 211 | @methods = {} 212 | @when_counter = 0 213 | end 214 | 215 | def when(*args) 216 | clazz = AtomBuilder === args[0] ? nil : args.shift 217 | is_not = false 218 | is_collect = false 219 | mode = :equals 220 | while clazz.is_a? Symbol 221 | if clazz == :not || clazz == :~ 222 | is_not = true 223 | elsif clazz == :is_a? || clazz == :kind_of? || clazz == :instance_of? 224 | mode = :inherits 225 | elsif clazz == :collect 226 | is_collect = true 227 | elsif clazz == :exists? 228 | raise 'The \'exists\' quantifier is not yet supported.' 229 | end 230 | clazz = args.empty? ? nil : args.shift 231 | end 232 | 233 | if clazz == nil 234 | clazz = Object 235 | mode = :inherits 236 | end 237 | 238 | deftemplate = Core::Template.new clazz, mode 239 | atoms = [] 240 | @when_counter += 1 241 | htag = Symbol === args[0] ? args.shift : GeneratedTag.new 242 | head = Core::HeadAtom.new htag, deftemplate 243 | @tags[htag] = @when_counter 244 | 245 | args.each do |arg| 246 | if arg.kind_of? Hash 247 | arg.each do |ab,tag| 248 | ab.tag = tag 249 | ab.deftemplate = deftemplate 250 | @tags[tag] = @when_counter 251 | @methods[tag] = ab.name 252 | atoms.push *ab.build_atoms(@tags, @methods, @when_counter) 253 | end 254 | elsif arg.kind_of? AtomBuilder 255 | arg.tag = GeneratedTag.new 256 | arg.deftemplate = deftemplate 257 | @methods[arg.tag] = arg.name 258 | atoms.push *arg.build_atoms(@tags, @methods, @when_counter) 259 | elsif arg.kind_of? FunctionBuilder 260 | atoms.push arg.build_atom(GeneratedTag.new, deftemplate) 261 | elsif arg == false 262 | raise 'The != operator is not allowed.' 263 | else 264 | raise "Invalid condition: #{arg}" 265 | end 266 | end 267 | 268 | if is_not 269 | p = mode==:inherits ? Core::NotInheritsPattern.new(head, atoms) : 270 | Core::NotPattern.new(head, atoms) 271 | else 272 | p = mode==:inherits ? Core::InheritsPattern.new(head, atoms) : 273 | is_collect ? Core::CollectPattern.new(head, atoms) : 274 | Core::ObjectPattern.new(head, atoms) 275 | end 276 | @pattern = @pattern ? Core::AndPattern.new(@pattern, p) : p 277 | end 278 | 279 | def then(&block) 280 | @action = Core::Action.new(&block) 281 | @action.name = @name 282 | @action.priority = @priority 283 | end 284 | 285 | def priority 286 | return @priority 287 | end 288 | 289 | def priority=(p) 290 | @priority = p 291 | @action.priority = @priority 292 | end 293 | 294 | def build_rule 295 | Core::Rule.new @name, @pattern, @action, @priority 296 | end 297 | end 298 | 299 | class MethodBuilder 300 | public_instance_methods.each do |m| 301 | # maybe we shouldn't be undefing object_id. What are the implications? Can we make object_id a 302 | # pass through to the underlying object's object_id? 303 | a = [:method_missing, :new, :public_instance_methods, :__send__, :__id__, :object_id] 304 | undef_method m.to_sym unless a.include? m.to_sym 305 | end 306 | 307 | def method_missing(method_id, *args, &block) 308 | ab = AtomBuilder.new method_id 309 | if block_given? 310 | args.each do |arg| 311 | ab.bindings.push BindingBuilder.new(arg, method_id) 312 | end 313 | ab.block = block 314 | elsif args.size > 0 315 | puts args.class.to_s + ' --- ' + args.to_s 316 | raise 'Arguments not supported for short-hand conditions' 317 | end 318 | ab 319 | end 320 | end 321 | 322 | class FunctionBuilder 323 | def initialize(args, block) 324 | @args = args 325 | @function = block 326 | end 327 | 328 | def build_atom(tag, template) 329 | Core::FunctionAtom.new(tag, template, @args, @function) 330 | end 331 | end 332 | 333 | class BindingBuilder 334 | attr_accessor :tag, :method 335 | def initialize(tag,method=nil) 336 | @tag = tag 337 | @method = method 338 | end 339 | 340 | def +(arg) 341 | raise 'Cannot use operators in short-hand mode!' 342 | end 343 | 344 | def -(arg) 345 | raise 'Cannot use operators in short-hand mode!' 346 | end 347 | 348 | def /(arg) 349 | raise 'Cannot use operators in short-hand mode!' 350 | end 351 | 352 | def *(arg) 353 | raise 'Cannot use operators in short-hand mode!' 354 | end 355 | 356 | def to_s 357 | "BindingBuilder @tag=#{@tag}, @method=#{@method}" 358 | end 359 | end 360 | 361 | class AtomBuilder 362 | attr_accessor :tag, :name, :bindings, :deftemplate, :block 363 | 364 | EQ_PROC = lambda {|x,y| x and x == y} 365 | GT_PROC = lambda {|x,y| x and x > y} 366 | LT_PROC = lambda {|x,y| x and x < y} 367 | MATCH_PROC = lambda {|x,y| x and x =~ y} 368 | LTE_PROC = lambda {|x,y| x and x <= y} 369 | GTE_PROC = lambda {|x,y| x and x >= y} 370 | TRUE_PROC = lambda {|x| true} 371 | 372 | def initialize(method_id) 373 | @name = method_id 374 | @deftemplate = nil 375 | @tag = GeneratedTag.new 376 | @bindings = [] 377 | @block = TRUE_PROC 378 | @child_atom_builders = [] 379 | end 380 | 381 | def method_missing(method_id, *args, &block) 382 | if method_id == :not 383 | NotOperatorBuilder.new(@name) 384 | end 385 | end 386 | 387 | def ==(value) 388 | @atom_type = :equals 389 | create_block value, EQ_PROC 390 | self 391 | end 392 | 393 | def >(value) 394 | create_block value, GT_PROC 395 | self 396 | end 397 | 398 | def <(value) 399 | create_block value, LT_PROC 400 | self 401 | end 402 | 403 | def =~(value) 404 | create_block value, MATCH_PROC 405 | self 406 | end 407 | 408 | def <=(value) 409 | create_block value, LTE_PROC 410 | self 411 | end 412 | 413 | def >=(value) 414 | create_block value, GTE_PROC 415 | self 416 | end 417 | 418 | def build_atoms(tags,methods,when_id) 419 | atoms = @child_atom_builders.map { |atom_builder| 420 | tags[atom_builder.tag] = when_id 421 | methods[atom_builder.tag] = atom_builder.name 422 | atom_builder.build_atoms(tags,methods,when_id) 423 | }.flatten || [] 424 | 425 | if @bindings.empty? 426 | if @atom_type == :equals 427 | return atoms << Core::EqualsAtom.new(@tag, @name, @deftemplate, @value) 428 | else 429 | return atoms << Core::PropertyAtom.new(@tag, @name, @deftemplate, @value, @block) 430 | end 431 | end 432 | 433 | if references_self?(tags,when_id) 434 | bind_methods = @bindings.collect{ |bb| methods[bb.tag] } 435 | atoms << Core::SelfReferenceAtom.new(@tag,@name,bind_methods,@deftemplate,@block) 436 | else 437 | bind_tags = @bindings.collect{ |bb| bb.tag } 438 | atoms << Core::ReferenceAtom.new(@tag,@name,bind_tags,@deftemplate,@block) 439 | end 440 | end 441 | 442 | private 443 | def references_self?(tags,when_id) 444 | ref_self = 0 445 | @bindings.each do |bb| 446 | if (tags[bb.tag] == when_id) 447 | ref_self += 1 448 | end 449 | end 450 | 451 | if ref_self > 0 and ref_self != @bindings.size 452 | raise 'Binding to self and another pattern in the same condition is not yet supported.' 453 | end 454 | 455 | ref_self > 0 456 | end 457 | 458 | def create_block(value, block) 459 | @block = block 460 | if value && value.kind_of?(BindingBuilder) 461 | @bindings = [value] 462 | elsif value && value.kind_of?(AtomBuilder) 463 | @child_atom_builders << value 464 | @bindings = [BindingBuilder.new(value.tag)] 465 | else 466 | @value = value 467 | end 468 | end 469 | end 470 | 471 | class NotOperatorBuilder < AtomBuilder 472 | NOT_PROC = lambda {|x,y| x != y} 473 | def ==(value) 474 | create_block value, NOT_PROC 475 | self 476 | end 477 | end 478 | 479 | class OrBuilder 480 | attr_reader :conditions 481 | def initialize(conditions) 482 | @conditions = conditions 483 | end 484 | end 485 | 486 | class AndBuilder 487 | attr_reader :conditions 488 | def initialize(conditions) 489 | @conditions = conditions 490 | end 491 | end 492 | end 493 | end 494 | -------------------------------------------------------------------------------- /lib/core/utils.rb: -------------------------------------------------------------------------------- 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 Core 14 | 15 | # This class is used as a unique fact that is assert to an engine's working memory 16 | # immediately after creation. This fact is used mainly when a NotPattern is put 17 | # at the begining of a rule. This allows it to join the 'not' to something tangible. 18 | class InitialFact 19 | 20 | end 21 | 22 | # Appearently Ruby doesn't have any kind of Exception chaining. So this class will have 23 | # fill the gap for Ruleby. 24 | class ProcessInvocationError < StandardError 25 | def initialize(root_cause) 26 | @root_cause = root_cause 27 | end 28 | 29 | def backtrace 30 | @root_cause.backtrace 31 | end 32 | 33 | def inspect 34 | @root_cause.inspect 35 | end 36 | 37 | def to_s 38 | @root_cause.to_s 39 | end 40 | end 41 | 42 | # This class is a wrapper for the context under which the network executes for 43 | # for a given fact. It is essentially a wrapper for a fact and a partial 44 | # match. 45 | class MatchContext 46 | 47 | attr_reader:fact 48 | attr_reader:match 49 | 50 | def initialize(fact,mr) 51 | @fact = fact 52 | @match = mr 53 | end 54 | 55 | def to_s 56 | return @match.to_s 57 | end 58 | 59 | def ==(t) 60 | return t && @fact == t.fact && @match == t.match 61 | end 62 | end 63 | 64 | # This class represents a partial match. It contains the variables, values, 65 | # and some metadata about the match. For the most part, this metadata is used 66 | # during conflict resolution. 67 | class MatchResult 68 | # TODO this class needs to be cleaned up so that we don't have a bunch of 69 | # properties. Instead, maybe it sould have a list of facts. 70 | 71 | attr :variables, true 72 | attr :is_match, true 73 | attr :fact_hash, true 74 | attr :recency, true 75 | 76 | def initialize(variables=Hash.new,is_match=false,fact_hash={},recency=[]) 77 | @variables = variables 78 | 79 | # a list of recencies of the facts that this matchresult depends on. 80 | @recency = recency 81 | 82 | # notes where this match result is from a NotPattern or ObjectPattern 83 | # TODO this isn't really needed anymore. how can we get rid of it? 84 | @is_match = is_match 85 | 86 | # a hash of fact.ids that each tag corresponds to 87 | @fact_hash = fact_hash 88 | end 89 | 90 | def []=(sym, object) 91 | @variables[sym] = object 92 | end 93 | 94 | def [](sym) 95 | return @variables[sym] 96 | end 97 | 98 | def fact_ids 99 | return fact_hash.values.uniq 100 | end 101 | 102 | def ==(match) 103 | return match != nil && @variables == match.variables && @is_match == match.is_match && @fact_hash == match.fact_hash 104 | end 105 | 106 | def key?(m) 107 | return @variables.key?(m) 108 | end 109 | 110 | def keys 111 | return @variables.keys 112 | end 113 | 114 | def update(mr) 115 | @recency = @recency | mr.recency 116 | @is_match = mr.is_match 117 | @variables = @variables.update mr.variables 118 | @fact_hash = @fact_hash.update mr.fact_hash 119 | return self 120 | end 121 | 122 | def dup 123 | dup_mr = MatchResult.new 124 | dup_mr.recency = @recency.clone 125 | dup_mr.is_match = @is_match 126 | dup_mr.variables = @variables.clone 127 | dup_mr.fact_hash = @fact_hash.clone 128 | return dup_mr 129 | end 130 | 131 | def merge!(mr) 132 | return update(mr) 133 | end 134 | 135 | def merge(mr) 136 | new_mr = MatchResult.new 137 | new_mr.recency = @recency | mr.recency 138 | new_mr.is_match = mr.is_match 139 | new_mr.variables = @variables.merge mr.variables 140 | new_mr.fact_hash = @fact_hash.merge mr.fact_hash 141 | return new_mr 142 | end 143 | 144 | def clear 145 | @variables = {} 146 | @fact_hash = {} 147 | @recency = [] 148 | end 149 | 150 | def delete(tag) 151 | @variables.delete(tag) 152 | @fact_hash.delete(tag) 153 | end 154 | 155 | def to_s 156 | s = '#MatchResult(' 157 | s = s + 'f)(' unless @is_match 158 | s = s + object_id.to_s+')(' 159 | @variables.each do |key,value| 160 | s += "#{key}=#{value}/#{@fact_hash[key]}, " 161 | end 162 | return s + ")" 163 | end 164 | end 165 | 166 | # This class is used when we need to have a Hash where keys and values are 167 | # mapped many-to-many. This class allows for quick access of both key and 168 | # value. It is similar to Multimap in C++ standard lib. 169 | # This thing is a mess (and barely works). It needs to be refactored. 170 | class MultiHash 171 | def initialize(key=nil, values=[]) 172 | @i = 0 173 | clear 174 | if key 175 | @keys = {key => []} 176 | values.each do |v| 177 | xref = generate_xref() 178 | xref_list = @keys[key] 179 | xref_list.push xref 180 | @keys[key] = xref_list 181 | @values = {xref => v} 182 | @backward_hash = {xref => [key]} 183 | end 184 | end 185 | end 186 | 187 | def empty? 188 | return @keys.empty? 189 | end 190 | 191 | def rehash 192 | @keys.rehash 193 | @values.rehash 194 | @backward_hash.rehash 195 | end 196 | 197 | def value?(mr) 198 | @values.value?(mr) 199 | end 200 | 201 | def clear 202 | @keys = {} 203 | @values = {} 204 | @backward_hash = {} 205 | end 206 | 207 | def values_by_id(id) 208 | xrefs = @keys[id] 209 | values = [] 210 | if xrefs 211 | xrefs.each do |k| 212 | values.push @values[k] 213 | end 214 | else 215 | #??? 216 | end 217 | return values 218 | end 219 | 220 | def each_key 221 | @keys.each_key do |key| 222 | yield(key) 223 | end 224 | end 225 | 226 | def has_key?(key) 227 | return @keys.has_key?(key) 228 | end 229 | 230 | def key?(key) 231 | return has_key?(key) 232 | end 233 | 234 | def +(dh) 235 | # TODO this can be faster 236 | new_dh = dh.dup 237 | dh.concat self.dup 238 | return new_dh 239 | end 240 | 241 | def add(ids,val) 242 | xref = generate_xref() 243 | ids.each do |id| 244 | xref_list = @keys[id] 245 | xref_list = [] if xref_list == @keys.default 246 | xref_list.push xref 247 | @keys[id] = xref_list 248 | end 249 | @values[xref] = val 250 | @backward_hash[xref] = ids 251 | end 252 | 253 | # DEPRECATED 254 | # WARN this method adds a value to the MultiHash only if it is unique. It 255 | # can be a fairly costly operation, and should be avoided. We only 256 | # implemented this as part of a hack to get things working early on. 257 | def add_uniq(ids,val) 258 | xref = generate_xref() 259 | exist_list = [] 260 | ids.each do |id| 261 | xref_list = @keys[id] 262 | if xref_list != @keys.default 263 | xref_list.each do |existing_xref| 264 | existing_val = @values[existing_xref] 265 | if existing_val 266 | if val == existing_val 267 | xref = existing_xref 268 | exist_list.push id 269 | break 270 | end 271 | else 272 | # HACK there shouldn't be any xrefs like this in the 273 | # hash to being with. Why are they there? 274 | xref_list.delete(existing_xref) 275 | @keys[id] = xref_list 276 | end 277 | end 278 | end 279 | end 280 | add_list = ids - exist_list 281 | add_list.each do |id| 282 | xref_list = @keys[id] 283 | xref_list = [] if xref_list == @keys.default 284 | xref_list.push xref 285 | @keys[id] = xref_list 286 | end 287 | @values[xref] = val if exist_list.empty? 288 | b_list = @backward_hash[xref] 289 | if b_list 290 | @backward_hash[xref] = b_list | ids 291 | else 292 | @backward_hash[xref] = ids 293 | end 294 | end 295 | 296 | def each 297 | @values.each do |xref,val| 298 | ids = @backward_hash[xref] 299 | yield(ids,val) 300 | end 301 | end 302 | 303 | def each_internal 304 | @values.each do |xref,val| 305 | ids = @backward_hash[xref] 306 | yield(ids,xref,val) 307 | end 308 | end 309 | private:each_internal 310 | 311 | def concat(multi_hash) 312 | multi_hash.each do |ids,val| 313 | add(ids,val) 314 | end 315 | end 316 | 317 | # DEPRECATED 318 | # WARN see comments in add_uniq 319 | def concat_uniq(double_hash) 320 | double_hash.each do |ids,val| 321 | add_uniq(ids,val) 322 | end 323 | end 324 | 325 | def default 326 | return @values.default 327 | end 328 | 329 | def remove(id) 330 | xref_list = @keys.delete(id) 331 | if xref_list != @keys.default 332 | removed_values = [] 333 | xref_list.each do |xref| 334 | value = @values.delete(xref) 335 | removed_values.push value 336 | id_list = @backward_hash.delete(xref) 337 | id_list.each do |next_id| 338 | remove_internal(next_id,xref) if next_id != id 339 | end 340 | end 341 | return removed_values 342 | else 343 | # puts 'WARN: tried to remove from MultiHash where id does not exist' 344 | return default 345 | end 346 | end 347 | 348 | def remove_internal(id,xref) 349 | xref_list = @keys[id] 350 | if xref_list # BUG this shouldn't be nil! 351 | xref_list.delete(xref) 352 | if xref_list.empty? 353 | @keys.delete(id) 354 | else 355 | @keys[id] = xref_list 356 | end 357 | end 358 | end 359 | private:remove_internal 360 | 361 | def remove_by_xref(ids,xref) 362 | ids.each do |id| 363 | xref_list = @keys[id] 364 | xref_list.delete(xref) 365 | if xref_list.empty? 366 | @keys.delete(id) 367 | else 368 | @keys[id] = xref_list 369 | end 370 | end 371 | @values.delete(xref) 372 | @backward_hash.delete(xref) 373 | end 374 | private:remove_by_xref 375 | 376 | def delete_if 377 | @values.delete_if do |xref,v| 378 | if yield(v) 379 | id_list = @backward_hash.delete(xref) 380 | id_list.each do |next_id| 381 | remove_internal(next_id,xref) 382 | end 383 | true 384 | else 385 | false 386 | end 387 | end 388 | end 389 | 390 | def values 391 | return @values.values 392 | end 393 | 394 | def keys 395 | return @keys.keys 396 | end 397 | 398 | def dup 399 | dup_mc = MultiHash.new 400 | each do |ids,v| 401 | dup_mc.add ids, v.dup 402 | end 403 | return dup_mc 404 | end 405 | 406 | def generate_xref() 407 | @i = @i + 1 408 | return @i 409 | end 410 | private:generate_xref 411 | 412 | # This method is for testing. It ensures that all the Hash's 413 | # and Array's are in order, and not corrupted (ex. some key points 414 | # to a xref that does not exist in the match_results Hash). 415 | def valid? 416 | @keys.each do |id,xrefs| 417 | # xref_list = @keys[id] 418 | # if xref_list != @keys.default 419 | # xref_list.each do |xref| 420 | # id_list = @backward_hash[xref] 421 | # unless id_list 422 | # puts 'yup' 423 | # return false 424 | # end 425 | # end 426 | # end 427 | xrefs.each do |xref| 428 | count = 0 429 | xrefs.each do |xref2| 430 | if xref == xref2 431 | count = count + 1 432 | if count > 1 433 | puts '(0) Duplicate xrefs in entry for keys' 434 | return false 435 | end 436 | end 437 | end 438 | 439 | mr = @match_results[xref] 440 | if mr == @match_results.default 441 | puts '(1) Missing entry in @match_results for xref' 442 | return false 443 | end 444 | 445 | # @match_results.each do |mr_xref,other_mr| 446 | # if other_mr == mr && mr_xref != xref 447 | # puts '(1a) Duplicate entry in @match_results' 448 | # return false 449 | # end 450 | # end 451 | 452 | id_list = @backward_hash[xref] 453 | if id_list == @backward_hash.default 454 | puts '(2) Missing entry in backward_hash for xref' 455 | return false 456 | end 457 | 458 | if id_list.index(id) == nil 459 | puts '(3) Entry in backward_hash is missing id' 460 | return false 461 | end 462 | 463 | id_list.each do |ref_id| 464 | unless ref_id == id 465 | ref_xref_list = @keys[ref_id] 466 | if ref_xref_list == @keys.default 467 | puts '(4) Missing entry in keys for backward_hash id' 468 | puts "#{id},#{mr},#{xref},#{ref_id}" 469 | return false 470 | end 471 | 472 | if ref_xref_list.index(xref) == nil 473 | puts '(5) Entry in keys is missing xref' 474 | puts "#{id},#{mr},#{xref},#{ref_id}" 475 | return false 476 | end 477 | end 478 | end 479 | end 480 | end 481 | return true 482 | end 483 | private:valid? 484 | 485 | def ==(dh) 486 | # TODO need to implement this 487 | return super 488 | end 489 | end 490 | end 491 | end -------------------------------------------------------------------------------- /GPL.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 19yy 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 309 | 310 | 311 | Also add information on how to contact you by electronic and paper mail. 312 | 313 | If the program is interactive, make it output a short notice like this 314 | when it starts in an interactive mode: 315 | 316 | Gnomovision version 69, Copyright (C) 19yy name of author 317 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 318 | This is free software, and you are welcome to redistribute it 319 | under certain conditions; type `show c' for details. 320 | 321 | The hypothetical commands `show w' and `show c' should show the appropriate 322 | parts of the General Public License. Of course, the commands you use may 323 | be called something other than `show w' and `show c'; they could even be 324 | mouse-clicks or menu items--whatever suits your program. 325 | 326 | You should also get your employer (if you work as a programmer) or your 327 | school, if any, to sign a "copyright disclaimer" for the program, if 328 | necessary. Here is a sample; alter the names: 329 | 330 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 331 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 332 | 333 | , 1 April 1989 334 | Ty Coon, President of Vice 335 | 336 | This General Public License does not permit incorporating your program into 337 | proprietary programs. If your program is a subroutine library, you may 338 | consider it more useful to permit linking proprietary applications with the 339 | library. If this is what you want to do, use the GNU Library General 340 | Public License instead of this License. 341 | 342 | -------------------------------------------------------------------------------- /spec/collect_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class A 4 | attr :value, true 5 | def initialize(v=nil); @value = v; end 6 | end 7 | 8 | class B 9 | attr :value1, true 10 | attr :value2, true 11 | def initialize(v1=nil, v2=nil); @value1 = v1; @value2 = v2; end 12 | end 13 | 14 | class C 15 | 16 | end 17 | 18 | include Ruleby 19 | 20 | class CollectRulebook < Rulebook 21 | def rules_with_one_pattern 22 | rule [:collect, A, :a] do |v| 23 | assert v[:a] 24 | assert Success.new 25 | end 26 | end 27 | 28 | def rules_with_one_pattern_and_other_conditions 29 | rule [:collect, A, :a, m.value == "foo"] do |v| 30 | assert v[:a] 31 | assert Success.new 32 | end 33 | 34 | rule [:collect, B, :b, m.value1 == "foo", m.value2 == "bar"] do |v| 35 | assert v[:b] 36 | assert Success.new 37 | end 38 | end 39 | 40 | def rules_with_one_pattern_inside_and 41 | rule AND([:collect, A, :a]) do |v| 42 | assert v[:a] 43 | assert Success.new 44 | end 45 | end 46 | 47 | def rules_with_two_collect_patterns_of_same_type 48 | rule [:collect, A, :a1], [:collect, A, :a2] do |v| 49 | assert v[:a1] 50 | assert v[:a2] 51 | assert Success.new 52 | end 53 | end 54 | 55 | def rules_with_two_collect_patterns_of_different_type 56 | rule [:collect, A, :a], [:collect, B, :b] do |v| 57 | assert v[:a] 58 | assert v[:b] 59 | assert Success.new 60 | end 61 | end 62 | 63 | def rules_with_one_pattern_and_a_not_on_right 64 | rule [:collect, A, :a], [:not, B] do |v| 65 | assert v[:a] 66 | assert Success.new 67 | end 68 | end 69 | 70 | def rules_with_one_pattern_and_a_not_on_left 71 | rule [:not, B], [:collect, A, :a] do |v| 72 | assert v[:a] 73 | assert Success.new 74 | end 75 | end 76 | 77 | def rules_with_more_than_one_pattern 78 | rule [:collect, A, :a], [B, :b] do |v| 79 | assert v[:a] 80 | assert Success.new(:right) 81 | end 82 | 83 | rule [B, :b], [:collect, A, :a] do |v| 84 | assert v[:a] 85 | assert Success.new(:left) 86 | end 87 | end 88 | 89 | def rules_with_more_than_one_pattern_on_each_side 90 | rule [B, :b], [:collect, A, :a], [C, :c] do |v| 91 | assert v[:a] 92 | assert Success.new 93 | end 94 | end 95 | 96 | def rules_with_chaining 97 | rule [:collect, A, :a] do |v| 98 | assert v[:a] 99 | assert Success.new 100 | end 101 | 102 | rule [C, :c] do |v| 103 | assert A.new 104 | end 105 | end 106 | 107 | def rules_with_binding 108 | rule [:collect, A, :a], [B, :b, m.value1(:a, &c{|v1, a| a.size > 0 and a[0].object.value == v1})] do |v| 109 | assert Success.new 110 | end 111 | 112 | rule [:collect, A, :a], [B, :b, m.value1(:a, &c{|v1, a| a.size > 1 and a[0].object.value == v1})] do |v| 113 | assert Success.new 114 | end 115 | end 116 | 117 | def rules_with_chaining_and_binding 118 | rule [:collect, A, :a], [B, :b, m.value1(:a, &c{|v1, a| a.size > 0})] do |v| 119 | assert v[:a] 120 | assert Success.new 121 | end 122 | 123 | rule [C, :c] do |v| 124 | assert A.new 125 | end 126 | end 127 | end 128 | 129 | describe Ruleby::Core::Engine do 130 | 131 | describe ":collect" do 132 | 133 | shared_examples_for "not patterns with [:collect, A] and [:not, B] when there is a B" do 134 | it "should not match" do 135 | s = subject.retrieve Success 136 | s.should_not be_nil 137 | s.size.should == 0 138 | end 139 | 140 | it "should match once B is retracted" do 141 | a = subject.retrieve B 142 | subject.retract a[0] 143 | 144 | subject.match 145 | 146 | s = subject.retrieve Success 147 | s.size.should == 1 148 | a = subject.retrieve A 149 | a.size.should == 2 150 | end 151 | end 152 | 153 | shared_examples_for "one :collect A rule and one A" do 154 | it "should retrieve Success" do 155 | s = subject.retrieve Success 156 | s.should_not be_nil 157 | s.size.should == 1 158 | 159 | s = subject.retrieve Array 160 | s.should_not be_nil 161 | s.size.should == 1 162 | 163 | a = s[0] 164 | a.size.should == 1 165 | a[0].object.class.should == A 166 | end 167 | 168 | it "should retract without error" do 169 | s = subject.retrieve Success 170 | s.size.should == 1 171 | subject.retract s[0] 172 | 173 | a = subject.retrieve A 174 | a.size.should == 1 175 | subject.retract a[0] 176 | 177 | subject.match 178 | 179 | s = subject.retrieve Success 180 | s.size.should == 0 181 | a = subject.retrieve A 182 | a.size.should == 0 183 | end 184 | end 185 | 186 | shared_examples_for "one :collect A rule and two As" do 187 | it "should retrieve Success" do 188 | s = subject.retrieve Success 189 | s.should_not be_nil 190 | s.size.should == 1 191 | 192 | s = subject.retrieve Array 193 | s.should_not be_nil 194 | s.size.should == 1 195 | 196 | a = s[0] 197 | a.size.should == 2 198 | a[0].object.class.should == A 199 | a[1].object.class.should == A 200 | end 201 | 202 | it "should retract without error" do 203 | s = subject.retrieve Success 204 | subject.retract s[0] 205 | a = subject.retrieve A 206 | subject.retract a[0] 207 | 208 | subject.match 209 | 210 | s = subject.retrieve Success 211 | s.size.should == 1 212 | a = subject.retrieve A 213 | a.size.should == 1 214 | 215 | s = subject.retrieve Success 216 | subject.retract s[0] 217 | a = subject.retrieve A 218 | subject.retract a[0] 219 | 220 | subject.match 221 | 222 | s = subject.retrieve Success 223 | s.size.should == 0 224 | a = subject.retrieve A 225 | a.size.should == 0 226 | end 227 | end 228 | 229 | context "as one pattern" do 230 | subject do 231 | engine :engine do |e| 232 | CollectRulebook.new(e).rules_with_one_pattern 233 | end 234 | end 235 | 236 | context "with one A" do 237 | before do 238 | subject.assert A.new 239 | subject.match 240 | end 241 | 242 | it_should_behave_like "one :collect A rule and one A" 243 | end 244 | 245 | context "with more than one A" do 246 | before do 247 | subject.assert A.new 248 | subject.assert A.new 249 | subject.match 250 | end 251 | 252 | it_should_behave_like "one :collect A rule and two As" 253 | end 254 | end 255 | 256 | context "as one pattern and other conditions" do 257 | subject do 258 | engine :engine do |e| 259 | CollectRulebook.new(e).rules_with_one_pattern_and_other_conditions 260 | end 261 | end 262 | 263 | context "with one A('foo')" do 264 | before do 265 | subject.assert A.new("foo") 266 | subject.match 267 | end 268 | 269 | it_should_behave_like "one :collect A rule and one A" 270 | end 271 | 272 | context "with more than one A('foo')" do 273 | before do 274 | subject.assert A.new("foo") 275 | subject.assert A.new("foo") 276 | subject.match 277 | end 278 | 279 | it_should_behave_like "one :collect A rule and two As" 280 | end 281 | 282 | context "with one A('foo') and one A(nil)" do 283 | before do 284 | subject.assert A.new("foo") 285 | subject.assert A.new 286 | subject.match 287 | end 288 | 289 | it "should retrieve Success" do 290 | s = subject.retrieve Success 291 | s.should_not be_nil 292 | s.size.should == 1 293 | 294 | s = subject.retrieve Array 295 | s.should_not be_nil 296 | s.size.should == 1 297 | 298 | a = s[0] 299 | a.size.should == 1 300 | a[0].object.class.should == A 301 | end 302 | end 303 | 304 | context "with more than one A('foo') and one A(nil)" do 305 | before do 306 | subject.assert A.new("foo") 307 | subject.assert A.new("foo") 308 | subject.assert A.new 309 | subject.match 310 | end 311 | 312 | it "should retrieve Success" do 313 | s = subject.retrieve Success 314 | s.should_not be_nil 315 | s.size.should == 1 316 | 317 | s = subject.retrieve Array 318 | s.should_not be_nil 319 | s.size.should == 1 320 | 321 | a = s[0] 322 | a.size.should == 2 323 | a[0].object.class.should == A 324 | a[1].object.class.should == A 325 | end 326 | end 327 | 328 | context "with one A(nil)" do 329 | before do 330 | subject.assert A.new 331 | subject.match 332 | end 333 | 334 | it "should not succeed" do 335 | s = subject.retrieve Success 336 | s.should_not be_nil 337 | s.size.should == 0 338 | end 339 | end 340 | 341 | context "with one B(foo,nil)" do 342 | before do 343 | subject.assert B.new("foo") 344 | subject.match 345 | end 346 | 347 | it "should not succeed" do 348 | s = subject.retrieve Success 349 | s.should_not be_nil 350 | s.size.should == 0 351 | end 352 | end 353 | 354 | context "with one B(foo,bar)" do 355 | before do 356 | subject.assert B.new("foo", "bar") 357 | subject.match 358 | end 359 | 360 | it "should not succeed" do 361 | s = subject.retrieve Success 362 | s.should_not be_nil 363 | s.size.should == 1 364 | end 365 | end 366 | 367 | context "with one B(foo,bar) and one B(foo,nil)" do 368 | before do 369 | subject.assert B.new("foo", "bar") 370 | subject.assert B.new("foo", "bar") 371 | subject.assert B.new("foo") 372 | subject.match 373 | end 374 | 375 | it "should succeed" do 376 | s = subject.retrieve Success 377 | s.should_not be_nil 378 | s.size.should == 1 379 | 380 | s = subject.retrieve Array 381 | s.should_not be_nil 382 | s.size.should == 1 383 | 384 | a = s[0] 385 | a.size.should == 2 386 | a[0].object.class.should == B 387 | a[1].object.class.should == B 388 | end 389 | end 390 | end 391 | 392 | context "as one pattern inside AND" do 393 | subject do 394 | engine :engine do |e| 395 | CollectRulebook.new(e).rules_with_one_pattern_inside_and 396 | end 397 | end 398 | 399 | context "with one A" do 400 | before do 401 | subject.assert A.new 402 | subject.match 403 | end 404 | 405 | it_should_behave_like "one :collect A rule and one A" 406 | end 407 | 408 | context "with more than one A" do 409 | before do 410 | subject.assert A.new 411 | subject.assert A.new 412 | subject.match 413 | end 414 | 415 | it_should_behave_like "one :collect A rule and two As" 416 | end 417 | end 418 | 419 | context "as two patterns of same type" do 420 | subject do 421 | engine :engine do |e| 422 | CollectRulebook.new(e).rules_with_two_collect_patterns_of_same_type 423 | end 424 | end 425 | 426 | context "with one A" do 427 | before do 428 | subject.assert A.new 429 | subject.match 430 | end 431 | 432 | it "should match" do 433 | s = subject.retrieve Success 434 | s.should_not be_nil 435 | s.size.should == 1 436 | 437 | s = subject.retrieve Array 438 | s.should_not be_nil 439 | s.size.should == 2 440 | 441 | a = s[0] 442 | a.size.should == 1 443 | a[0].object.class.should == A 444 | 445 | a = s[1] 446 | a.size.should == 1 447 | a[0].object.class.should == A 448 | end 449 | end 450 | end 451 | 452 | context "as two patterns of different type" do 453 | subject do 454 | engine :engine do |e| 455 | CollectRulebook.new(e).rules_with_two_collect_patterns_of_different_type 456 | end 457 | end 458 | 459 | context "with one A" do 460 | before do 461 | subject.assert A.new 462 | subject.match 463 | end 464 | 465 | it "should not match" do 466 | s = subject.retrieve Success 467 | s.should_not be_nil 468 | s.size.should == 0 469 | end 470 | end 471 | 472 | context "with one A and one B" do 473 | before do 474 | subject.assert A.new 475 | subject.assert B.new 476 | subject.match 477 | end 478 | 479 | it "should not match" do 480 | s = subject.retrieve Success 481 | s.should_not be_nil 482 | s.size.should == 1 483 | 484 | s = subject.retrieve Array 485 | s.should_not be_nil 486 | s.size.should == 2 487 | 488 | classes = [] 489 | 490 | a = s[0] 491 | a.size.should == 1 492 | classes << a[0].object.class 493 | 494 | a = s[1] 495 | a.size.should == 1 496 | classes << a[0].object.class 497 | 498 | classes.should include(A, B) 499 | end 500 | end 501 | end 502 | 503 | context "as one pattern and a not on left" do 504 | subject do 505 | engine :engine do |e| 506 | CollectRulebook.new(e).rules_with_one_pattern_and_a_not_on_left 507 | end 508 | end 509 | 510 | context "with one A" do 511 | before do 512 | subject.assert A.new 513 | subject.match 514 | end 515 | 516 | it_should_behave_like "one :collect A rule and one A" 517 | end 518 | 519 | context "with more than one A" do 520 | before do 521 | subject.assert A.new 522 | subject.assert A.new 523 | subject.match 524 | end 525 | 526 | it_should_behave_like "one :collect A rule and two As" 527 | end 528 | 529 | context "with more than one A and a B" do 530 | before do 531 | subject.assert A.new 532 | subject.assert A.new 533 | subject.assert B.new 534 | subject.match 535 | end 536 | 537 | it_should_behave_like "not patterns with [:collect, A] and [:not, B] when there is a B" 538 | end 539 | 540 | context "with more than one A and a B" do 541 | before do 542 | subject.assert B.new 543 | subject.assert A.new 544 | subject.assert A.new 545 | subject.match 546 | end 547 | 548 | it_should_behave_like "not patterns with [:collect, A] and [:not, B] when there is a B" 549 | end 550 | end 551 | 552 | context "as one pattern and a not on right" do 553 | subject do 554 | engine :engine do |e| 555 | CollectRulebook.new(e).rules_with_one_pattern_and_a_not_on_right 556 | end 557 | end 558 | 559 | context "with one A" do 560 | before do 561 | subject.assert A.new 562 | subject.match 563 | end 564 | 565 | it_should_behave_like "one :collect A rule and one A" 566 | end 567 | 568 | context "with more than one A" do 569 | before do 570 | subject.assert A.new 571 | subject.assert A.new 572 | subject.match 573 | end 574 | 575 | it_should_behave_like "one :collect A rule and two As" 576 | end 577 | 578 | context "with more than one A's first and a B" do 579 | before do 580 | subject.assert A.new 581 | subject.assert A.new 582 | subject.assert B.new 583 | subject.match 584 | end 585 | 586 | it_should_behave_like "not patterns with [:collect, A] and [:not, B] when there is a B" 587 | end 588 | 589 | context "with more than one A and a B first" do 590 | before do 591 | subject.assert B.new 592 | subject.assert A.new 593 | subject.assert A.new 594 | subject.match 595 | end 596 | 597 | it_should_behave_like "not patterns with [:collect, A] and [:not, B] when there is a B" 598 | end 599 | end 600 | 601 | context "as two patterns" do 602 | subject do 603 | engine :engine do |e| 604 | CollectRulebook.new(e).rules_with_more_than_one_pattern 605 | end 606 | end 607 | 608 | context "with one A" do 609 | before do 610 | subject.assert A.new 611 | subject.assert B.new 612 | subject.match 613 | end 614 | 615 | it "should retrieve Success" do 616 | s = subject.retrieve Success 617 | s.should_not be_nil 618 | s.size.should == 2 619 | 620 | s = subject.retrieve Array 621 | s.should_not be_nil 622 | s.size.should == 2 623 | 624 | a = s[0] 625 | a.size.should == 1 626 | a[0].object.class.should == A 627 | 628 | a = s[1] 629 | a.size.should == 1 630 | a[0].object.class.should == A 631 | end 632 | end 633 | 634 | context "with more than one A" do 635 | before do 636 | subject.assert A.new 637 | subject.assert B.new 638 | subject.assert A.new 639 | subject.match 640 | end 641 | 642 | it "should retrieve Success" do 643 | s = subject.retrieve Success 644 | s.should_not be_nil 645 | s.size.should == 2 646 | 647 | s = subject.retrieve Array 648 | s.should_not be_nil 649 | s.size.should == 2 # one array for each rule 650 | 651 | a = s[0] 652 | a.size.should == 2 653 | a[0].object.class.should == A 654 | a[1].object.class.should == A 655 | 656 | a = s[1] 657 | a.size.should == 2 658 | a[0].object.class.should == A 659 | a[1].object.class.should == A 660 | end 661 | 662 | it "should retract A without error" do 663 | s = subject.retrieve Success 664 | subject.retract s[0] 665 | subject.retract s[1] 666 | a = subject.retrieve A 667 | subject.retract a[0] 668 | 669 | subject.match 670 | 671 | s = subject.retrieve Success 672 | s.size.should == 2 673 | a = subject.retrieve A 674 | a.size.should == 1 675 | 676 | s = subject.retrieve Success 677 | subject.retract s[0] 678 | subject.retract s[1] 679 | a = subject.retrieve A 680 | subject.retract a[0] 681 | 682 | subject.match 683 | 684 | s = subject.retrieve Success 685 | s.size.should == 0 686 | a = subject.retrieve A 687 | a.size.should == 0 688 | end 689 | 690 | it "should retract B without error" do 691 | s = subject.retrieve Success 692 | subject.retract s[0] 693 | subject.retract s[1] 694 | b = subject.retrieve B 695 | subject.retract b[0] 696 | 697 | subject.match 698 | 699 | s = subject.retrieve Success 700 | s.size.should == 0 701 | a = subject.retrieve A 702 | a.size.should == 2 703 | b = subject.retrieve B 704 | b.size.should == 0 705 | end 706 | end 707 | end 708 | 709 | context "as patterns on each side" do 710 | subject do 711 | engine :engine do |e| 712 | CollectRulebook.new(e).rules_with_more_than_one_pattern_on_each_side 713 | end 714 | end 715 | 716 | context "with one A" do 717 | context "and no C" do 718 | before do 719 | subject.assert A.new 720 | subject.assert B.new 721 | subject.match 722 | end 723 | 724 | it "should retrieve Success" do 725 | s = subject.retrieve Success 726 | s.should_not be_nil 727 | s.size.should == 0 728 | end 729 | end 730 | 731 | context "and all other facts" do 732 | before do 733 | subject.assert A.new 734 | subject.assert B.new 735 | subject.assert C.new 736 | subject.match 737 | end 738 | 739 | it "should retrieve Success" do 740 | s = subject.retrieve Success 741 | s.should_not be_nil 742 | s.size.should == 1 743 | 744 | s = subject.retrieve Array 745 | s.should_not be_nil 746 | s.size.should == 1 747 | 748 | a = s[0] 749 | a.size.should == 1 750 | a[0].object.class.should == A 751 | end 752 | end 753 | end 754 | 755 | context "with more than one A" do 756 | before do 757 | subject.assert A.new 758 | subject.assert B.new 759 | subject.assert C.new 760 | subject.assert A.new 761 | subject.match 762 | end 763 | 764 | it "should retrieve Success" do 765 | s = subject.retrieve Success 766 | s.should_not be_nil 767 | s.size.should == 1 768 | 769 | s = subject.retrieve Array 770 | s.should_not be_nil 771 | s.size.should == 1 772 | 773 | a = s[0] 774 | a.size.should == 2 775 | a[0].object.class.should == A 776 | a[1].object.class.should == A 777 | end 778 | 779 | it "should retract A without error" do 780 | s = subject.retrieve Success 781 | subject.retract s[0] 782 | a = subject.retrieve A 783 | subject.retract a[0] 784 | 785 | subject.match 786 | 787 | s = subject.retrieve Success 788 | s.size.should == 1 789 | a = subject.retrieve A 790 | a.size.should == 1 791 | 792 | s = subject.retrieve Success 793 | subject.retract s[0] 794 | a = subject.retrieve A 795 | subject.retract a[0] 796 | 797 | subject.match 798 | 799 | s = subject.retrieve Success 800 | s.size.should == 0 801 | a = subject.retrieve A 802 | a.size.should == 0 803 | end 804 | 805 | it "should retract B without error" do 806 | s = subject.retrieve Success 807 | subject.retract s[0] 808 | b = subject.retrieve B 809 | subject.retract b[0] 810 | 811 | subject.match 812 | 813 | s = subject.retrieve Success 814 | s.size.should == 0 815 | a = subject.retrieve A 816 | a.size.should == 2 817 | b = subject.retrieve B 818 | b.size.should == 0 819 | end 820 | 821 | it "should retract C without error" do 822 | s = subject.retrieve Success 823 | subject.retract s[0] 824 | b = subject.retrieve C 825 | subject.retract b[0] 826 | 827 | subject.match 828 | 829 | s = subject.retrieve Success 830 | s.size.should == 0 831 | a = subject.retrieve A 832 | a.size.should == 2 833 | b = subject.retrieve C 834 | b.size.should == 0 835 | end 836 | end 837 | end 838 | 839 | context "as rule chain" do 840 | subject do 841 | engine :engine do |e| 842 | CollectRulebook.new(e).rules_with_chaining 843 | end 844 | end 845 | 846 | context "with one C" do 847 | before do 848 | subject.assert C.new 849 | subject.match 850 | end 851 | 852 | it "should retrieve Success" do 853 | s = subject.retrieve Success 854 | s.should_not be_nil 855 | s.size.should == 1 856 | 857 | s = subject.retrieve Array 858 | s.should_not be_nil 859 | s.size.should == 1 860 | 861 | a = s[0] 862 | a.size.should == 1 863 | a[0].object.class.should == A 864 | end 865 | end 866 | 867 | context "with many C's" do 868 | before do 869 | subject.assert C.new 870 | subject.assert C.new 871 | subject.assert C.new 872 | subject.assert C.new 873 | subject.assert C.new 874 | subject.match 875 | end 876 | 877 | it "should retrieve Success" do 878 | s = subject.retrieve Success 879 | s.should_not be_nil 880 | s.size.should == 1 881 | 882 | s = subject.retrieve Array 883 | s.should_not be_nil 884 | s.size.should == 1 885 | 886 | a = s[0] 887 | a.size.should == 5 888 | a[0].object.class.should == A 889 | end 890 | end 891 | end 892 | 893 | context "with rule binding" do 894 | subject do 895 | engine :engine do |e| 896 | CollectRulebook.new(e).rules_with_binding 897 | end 898 | end 899 | 900 | context "with one A and one B that have == values" do 901 | before do 902 | a = A.new 903 | a.value = 1 904 | b = B.new 905 | b.value1 = 1 906 | subject.assert b 907 | subject.assert a 908 | subject.match 909 | end 910 | 911 | it "should retrieve Success" do 912 | s = subject.retrieve Success 913 | s.size.should == 1 914 | end 915 | end 916 | 917 | context "with one A and one B that have == values asserted in reverse order" do 918 | before do 919 | a = A.new 920 | a.value = 1 921 | b = B.new 922 | b.value1 = 1 923 | subject.assert a 924 | subject.assert b 925 | subject.match 926 | end 927 | 928 | it "should retrieve Success" do 929 | s = subject.retrieve Success 930 | s.size.should == 1 931 | end 932 | end 933 | 934 | context "with one A and one B that have != values" do 935 | before do 936 | a = A.new 937 | a.value = 1 938 | b = B.new 939 | b.value1 = 42 940 | subject.assert a 941 | subject.assert b 942 | subject.match 943 | end 944 | 945 | it "should retrieve Success" do 946 | s = subject.retrieve Success 947 | s.size.should == 0 948 | end 949 | end 950 | 951 | context "with one A and one B that have != values" do 952 | before do 953 | a1 = A.new 954 | a1.value = 1 955 | b1 = B.new 956 | b1.value1 = 1 957 | a2 = A.new 958 | a2.value = 1 959 | subject.assert a1 960 | subject.assert b1 961 | subject.assert a2 962 | subject.match 963 | end 964 | 965 | it "should retrieve Success" do 966 | s = subject.retrieve Success 967 | s.size.should == 2 968 | end 969 | end 970 | end 971 | 972 | 973 | context "as rule chain with binding" do 974 | subject do 975 | engine :engine do |e| 976 | CollectRulebook.new(e).rules_with_chaining_and_binding 977 | end 978 | end 979 | 980 | context "with one B and one C" do 981 | before do 982 | subject.assert B.new 983 | subject.assert C.new 984 | subject.match 985 | end 986 | 987 | it "should retrieve Success" do 988 | subject.retrieve(Success).size.should == 1 989 | end 990 | end 991 | 992 | context "with many C's and two B" do 993 | before do 994 | subject.assert C.new 995 | subject.assert C.new 996 | subject.assert B.new 997 | subject.assert C.new 998 | subject.assert C.new 999 | subject.assert C.new 1000 | subject.assert B.new 1001 | subject.match 1002 | end 1003 | 1004 | it "should retrieve Success" do 1005 | subject.retrieve(Success).size.should == 2 1006 | 1007 | s = subject.retrieve Array 1008 | s.size.should == 2 1009 | 1010 | a = s[0] 1011 | a.size.should == 5 1012 | a.each {|o| o.object.class.should == A } 1013 | 1014 | a = s[1] 1015 | a.size.should == 5 1016 | a.each {|o| o.object.class.should == A } 1017 | end 1018 | end 1019 | end 1020 | end 1021 | end --------------------------------------------------------------------------------