diff --git a/lib/dsl/magnum.rb b/lib/dsl/magnum.rb new file mode 100644 index 0000000..411890f --- /dev/null +++ b/lib/dsl/magnum.rb @@ -0,0 +1,513 @@ +# This file is part of the Ruleby project (http://ruleby.org) +# +# This application is free software; you can redistribute it and/or +# modify it under the terms of the Ruby license defined in the +# LICENSE.txt file. +# +# Copyright (c) 2012 Joe Kutner and Matt Smith. All rights reserved. +# +# * Authors: Joe Kutner, Matt Smith +# + +module Ruleby + module Magnum + class RulebookHelper + + attr_reader :engine + + + + def self.name(n) + @@cur_name = n + end + + def self.desc(d) + @@cur_desc = d + end + + def self.opts(o) + @@cur_opts = o + end + + def self.reset_class_vars + @@cur_name, @@cur_desc, @@cur_opts = "default", '', {} + end + reset_class_vars + + def initialize(engine) + @engine = engine + end + + def rule(*args, &block) + name, desc, opts = pop_cur_name_desc_opts + + raise "Rule must have a name!" if name.nil? + + rules = Ruleby::Magnum.parse_containers(args, RulesContainer.new).build(name,opts,@engine,&block) + rules.each do |r| + engine.assert_rule(r) + end + end + + def where + WhereBuilder.new(&Proc.new) + end + + private + + def pop_cur_name_desc_opts + name = @@cur_name + desc = @@cur_desc + opts = @@cur_opts + RulebookHelper.reset_class_vars + return name, desc, opts + end + end + + def self.parse_containers(args, container=Container(:and), parent=nil) + con = nil + if(container.kind_of?(RulesContainer)) + con = Container.new(:and) + else + con = container + end + args.each do |arg| + if arg.kind_of? Array + con << PatternContainer.new(arg) + elsif arg.kind_of? AndBuilder + con << parse_containers(arg.conditions, Container.new(:and), container) + elsif arg.kind_of? OrBuilder + con << parse_containers(arg.conditions, Container.new(:or), container) + else + raise 'Invalid condition. Must be an OR, AND or an Array.' + end + end + if container.kind_of?(RulesContainer) + container << con + end + return container + end + + class RulesContainer < Array + def handle_branching + ands = [] + each do |x| + f = x.flatten_patterns + if f.or? + f.each do |o| + ands << o + end + else + ands << f + end + end + ands + end + + def build(name, options, engine, &block) + handle_branching.map do |container| + build_rule(name, container, options, &block) + end + end + + def build_rule(name, container, options, &block) + r = RuleBuilder.new name + container.build r + r.then(&block) + r.priority = options[:priority] if options[:priority] + r.build_rule + end + end + + class Container < Array + attr_accessor :kind + + def initialize(kind, *vals) + @kind = kind + self.push(*vals) + end + + def flatten_patterns + if or? + patterns = [] + each do |c| + f = c.flatten_patterns + if f.and? + patterns << f + else + f.each do |o| + # i hope this is safe... not entirely sure + patterns << (o.size == 1 ? o.first : o) + # patterns << o + end + end + end + + Container.new(:or, *patterns) + elsif and? + patterns = [] + or_patterns = [] + each do |c| + child_patterns = c.flatten_patterns + if child_patterns.or? and child_patterns.size > 1 + or_patterns << child_patterns + else + patterns.push(*child_patterns) + end + end + if or_patterns.empty? + flat = Container.new(:and) + flat.push(*patterns) + else + flat = Container.new(:or) + + x = or_patterns[1..-1] + if x.empty? + or_pattern_products = or_patterns[0].product() + else + or_pattern_products = or_patterns[0].product(*x) + end + + or_pattern_products.each do |op| + c = Container.new(:and) + c.push(*patterns) + c.push(*op) + flat << c + end + end + return flat + end + end + + def build(builder) + if self.or? + # OrContainers are never built, they just contain containers that + # will be transformed into AndContainers. + raise 'Invalid Syntax' + end + self.each do |x| + x.build builder + end + end + + def or? + return kind == :or + end + + def and? + return kind == :and + end + end + + class PatternContainer + def initialize(condition) + @condition = condition + end + + def size + 1 + end + + def first + self + end + + def flatten_patterns + Container.new(:and, self) + end + + def build(builder) + builder.when(*@condition) + end + + def process_tree + # there is no tree to process + false + end + + def or? + false + end + + def and? + false + end + end + + + class RuleBuilder + def initialize(name, pattern=nil, action=nil, priority=0) + @name = name + @pattern = pattern + @action = action + @priority = priority + + @tags = {} + @methods = {} + @when_counter = 0 + end + + def when(*args) + clazz = AtomBuilder === args[0] ? nil : args.shift + is_not = false + is_collect = false + mode = :equals + while clazz.is_a? Symbol + if clazz == :not || clazz == :~ + is_not = true + elsif clazz == :is_a? || clazz == :kind_of? || clazz == :instance_of? + mode = :inherits + elsif clazz == :collect + is_collect = true + elsif clazz == :exists? + raise 'The \'exists\' quantifier is not yet supported.' + end + clazz = args.empty? ? nil : args.shift + end + + if clazz == nil + clazz = Object + mode = :inherits + end + + deftemplate = Core::Template.new clazz, mode + atoms = [] + @when_counter += 1 + htag = Symbol === args[0] ? args.shift : GeneratedTag.new + head = Core::HeadAtom.new htag, deftemplate + @tags[htag] = @when_counter + + unless args.empty? + where_builder = args.last + where_builder.clauses.each do |clause| + # todo clause could besomething else + # - could be a hash for binding + # - could be an AND or OR + # - function builder? + if clause.kind_of? AtomBuilder + clause.deftemplate = deftemplate + @methods[clause.tag] = clause.name + atoms.push *clause.build_atoms(@tags, @methods, @when_counter) + elsif arg == false + raise 'The != operator is not allowed.' + else + raise "Invalid condition: #{arg}" + end + end + end + + if is_not + p = mode==:inherits ? Core::NotInheritsPattern.new(head, atoms) : + Core::NotPattern.new(head, atoms) + else + p = mode==:inherits ? Core::InheritsPattern.new(head, atoms) : + is_collect ? Core::CollectPattern.new(head, atoms) : + Core::ObjectPattern.new(head, atoms) + end + @pattern = @pattern ? Core::AndPattern.new(@pattern, p) : p + end + + def then(&block) + @action = Core::Action.new(&block) + @action.name = @name + @action.priority = @priority + end + + def priority + return @priority + end + + def priority=(p) + @priority = p + @action.priority = @priority + end + + def build_rule + Core::Rule.new @name, @pattern, @action, @priority + end + end + + class WhereBuilder + def clauses + @clause_builder.instance_eval do + puts 'returning #{@clauses.size} clauses' + return @clauses + end + end + + def initialize + @clause_builder = ClauseBuilder.new + @clause_builder.instance_eval(&Proc.new) + end + end + + class ClauseBuilder + + def initialize + @clauses = [] + end + + def method_missing(name, *args, &block) + raise "Args to where clause method not accepted yet :(" unless args.empty? + operation = AtomBuilder.new(name) + @clauses << operation + operation + end + end + + class AtomBuilder + attr_accessor :tag, :name, :bindings, :deftemplate, :block + + EQ_PROC = lambda {|x,y| x and x == y} + GT_PROC = lambda {|x,y| x and x > y} + LT_PROC = lambda {|x,y| x and x < y} + MATCH_PROC = lambda {|x,y| x and x =~ y} + LTE_PROC = lambda {|x,y| x and x <= y} + GTE_PROC = lambda {|x,y| x and x >= y} + TRUE_PROC = lambda {|x| true} + + def initialize(method_id) + @name = method_id + @deftemplate = nil + @tag = GeneratedTag.new + @bindings = [] + @block = TRUE_PROC + @child_atom_builders = [] + end + + def ==(value) + @atom_type = :equals + create_block value, EQ_PROC + self + end + + def >(value) + create_block value, GT_PROC + self + end + + def <(value) + create_block value, LT_PROC + self + end + + def =~(value) + create_block value, MATCH_PROC + self + end + + def <=(value) + create_block value, LTE_PROC + self + end + + def >=(value) + create_block value, GTE_PROC + self + end + + def build_atoms(tags,methods,when_id) + atoms = @child_atom_builders.map { |atom_builder| + tags[atom_builder.tag] = when_id + methods[atom_builder.tag] = atom_builder.name + atom_builder.build_atoms(tags,methods,when_id) + }.flatten || [] + + if @bindings.empty? + if @atom_type == :equals + return atoms << Core::EqualsAtom.new(@tag, @name, @deftemplate, @value) + else + return atoms << Core::PropertyAtom.new(@tag, @name, @deftemplate, @value, @block) + end + end + + if references_self?(tags,when_id) + bind_methods = @bindings.collect{ |bb| methods[bb.tag] } + atoms << Core::SelfReferenceAtom.new(@tag,@name,bind_methods,@deftemplate,@block) + else + bind_tags = @bindings.collect{ |bb| bb.tag } + atoms << Core::ReferenceAtom.new(@tag,@name,bind_tags,@deftemplate,@block) + end + end + + private + + def references_self?(tags,when_id) + ref_self = 0 + @bindings.each do |bb| + if (tags[bb.tag] == when_id) + ref_self += 1 + end + end + + if ref_self > 0 and ref_self != @bindings.size + raise 'Binding to self and another pattern in the same condition is not yet supported.' + end + + ref_self > 0 + end + + def create_block(value, block) + @block = block + if value && value.kind_of?(BindingBuilder) + @bindings = [value] + elsif value && value.kind_of?(AtomBuilder) + @child_atom_builders << value + @bindings = [BindingBuilder.new(value.tag)] + else + @value = value + end + end + end + + class BindingBuilder + attr_accessor :tag, :method + def initialize(tag,method=nil) + @tag = tag + @method = method + end + + def +(arg) + raise 'Cannot use operators in short-hand mode!' + end + + def -(arg) + raise 'Cannot use operators in short-hand mode!' + end + + def /(arg) + raise 'Cannot use operators in short-hand mode!' + end + + def *(arg) + raise 'Cannot use operators in short-hand mode!' + end + + def to_s + "BindingBuilder @tag=#{@tag}, @method=#{@method}" + end + end + + class NotOperatorBuilder < AtomBuilder + NOT_PROC = lambda {|x,y| x != y} + def ==(value) + create_block value, NOT_PROC + self + end + end + + class OrBuilder + attr_reader :conditions + def initialize(conditions) + @conditions = conditions + end + end + + class AndBuilder + attr_reader :conditions + def initialize(conditions) + @conditions = conditions + end + end + end +end diff --git a/lib/rule_helper.rb b/lib/rule_helper.rb index 50ada6b..6376620 100644 --- a/lib/rule_helper.rb +++ b/lib/rule_helper.rb @@ -20,52 +20,24 @@ def rule(*args, &block) end options = args[0].kind_of?(Hash) ? args.shift : {} - rules = Ruleby::Ferrari.parse_containers(args, Ruleby::Ferrari::RulesContainer.new).build(name,options,@engine,&block) + rules = Ruleby::Magnum.parse_containers(args, Ruleby::Magnum::RulesContainer.new).build(name,options,@engine,&block) rules end - def m - Ruleby::Ferrari::MethodBuilder.new + def where + Ruleby::Magnum::WhereBuilder.new(&Proc.new) end - def method - m - end - - def b(variable_name) - Ruleby::Ferrari::BindingBuilder.new(variable_name) - end - - def c(&block) - lambda(&block) - end + def name(n) - def f(args, block=nil) - if block.nil? - if !args.is_a?(Proc) - raise "You must provide a Proc!" - else - Ruleby::Ferrari::FunctionBuilder.new([], args) - end - else - if args.is_a?(Array) - Ruleby::Ferrari::FunctionBuilder.new(args, block) - else - Ruleby::Ferrari::FunctionBuilder.new([args], block) - end - end end def OR(*args) - Ruleby::Ferrari::OrBuilder.new args + Ruleby::Magnum::OrBuilder.new args end def AND(*args) - Ruleby::Ferrari::AndBuilder.new args - end - - def __eval__(x) - eval(x) + Ruleby::Magnum::AndBuilder.new args end end diff --git a/lib/rulebook.rb b/lib/rulebook.rb index d19999c..4c3dd6f 100644 --- a/lib/rulebook.rb +++ b/lib/rulebook.rb @@ -11,7 +11,7 @@ require 'ruleby' require 'rule_helper' -require 'dsl/ferrari' +require 'dsl/magnum' module Ruleby class Rulebook @@ -37,19 +37,8 @@ def rule(*args, &block) if args.empty? raise 'Must provide arguments to rule' else - name = args[0].kind_of?(Symbol) ? args.shift : GeneratedTag.new - i = args[0].kind_of?(Hash) ? 1 : 0 - if [Array, Ruleby::Ferrari::OrBuilder, Ruleby::Ferrari::AndBuilder].include? args[i].class - # use ferrari DSL - r = Ferrari::RulebookHelper.new @engine - r.rule name, *args, &block - elsif args[i].kind_of? String - # use letigre DSL - r = LeTigre::RulebookHelper.new @engine, self - r.rule name, *args, &block - else - raise 'Rule format not recognized.' - end + r = Ruleby::Magnum::RulebookHelper.new @engine + r.rule *args, &block end end end diff --git a/spec/magnum_spec.rb b/spec/magnum_spec.rb new file mode 100644 index 0000000..3e3930a --- /dev/null +++ b/spec/magnum_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +class A + +end + +include Ruleby + +class MagnumRulebook < Rulebook + def rules + name "test1" + rule [A] do |v| + assert Success.new + end + end +end + +describe Ruleby::Core::Engine do + + subject do + engine :engine do |e| + MagnumRulebook.new(e).rules + end + end + + describe "simple case" do + context "with one A" do + before do + subject.assert A.new + subject.match + end + + it "should retrieve Success" do + s = subject.retrieve Success + s.should_not be_nil + s.size.should == 1 + end + end + end +end \ No newline at end of file diff --git a/spec/property_spec.rb b/spec/property_spec.rb index d4c7af4..7abcf96 100644 --- a/spec/property_spec.rb +++ b/spec/property_spec.rb @@ -14,16 +14,16 @@ class PropCtx class PropRulebook < Rulebook def gt_rules - rule [PropFact, :p, m.value > 0] do + rule [PropFact, :p, where { self.value > 0}] do assert Success.new end end def lte_rules - rule [PropFact, :p, m.value > 42], [PropCtx, :pc] do + rule [PropFact, :p, where { self.value > 42 }], [PropCtx, :pc] do # do nothing, just being here helps reproduce a bug end - rule [PropFact, :p, m.value <= 42], [PropCtx, :pc] do + rule [PropFact, :p, where { self.value <= 42}], [PropCtx, :pc] do assert Success.new end end