Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

First implementation with functional specs - maybe I'll add unit spec…

…s later, but this already ensure the expected behavior
  • Loading branch information...
commit a3237386939bfcbadd8a84e4e3e5c59a8db80f40 0 parents
@lucashungaro authored
3  .gitignore
@@ -0,0 +1,3 @@
+pkg/*
+*.gem
+.bundle
4 Gemfile
@@ -0,0 +1,4 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in ruleset.gemspec
+gemspec
10 Rakefile
@@ -0,0 +1,10 @@
+require 'bundler'
+Bundler::GemHelper.install_tasks
+
+require 'spec/rake/spectask'
+Spec::Rake::SpecTask.new(:spec) do |spec|
+ spec.libs << 'lib' << 'spec'
+ spec.spec_files = FileList['spec/**/*_spec.rb']
+end
+
+task :default => :spec
27 lib/scoring_ruleset.rb
@@ -0,0 +1,27 @@
+require "scoring_ruleset/metaid"
+require "scoring_ruleset/ruleset"
+require "scoring_ruleset/rule"
+require "scoring_ruleset/condition"
+
+module ScoringRuleset
+ module ClassMethods
+ def scoring_ruleset
+ raise(ArgumentError, "Ruleset needs a block with the rules") unless block_given?
+ self.ruleset = Ruleset.new
+ yield self.ruleset
+ end
+ end
+
+ module InstanceMethods
+ def calculate_score
+ self.class.ruleset.evaluate(self)
+ end
+ end
+
+ def self.included(receiver)
+ receiver.extend ClassMethods
+ receiver.send :include, InstanceMethods
+
+ receiver.meta_eval { attr_accessor :ruleset }
+ end
+end
32 lib/scoring_ruleset/condition.rb
@@ -0,0 +1,32 @@
+class Condition
+ def initialize(type, method)
+ @type = type
+ @method = method
+ end
+
+ def evaluate(instance)
+ case @type
+ when :if
+ dispatch_pre_condition(instance)
+ when :unless
+ (dispatch_pre_condition(instance) - 1) * -1
+ when :each
+ dispatch_collection(instance)
+ end
+ end
+
+ private
+ def dispatch_pre_condition(instance)
+ result = 0
+ if @method.is_a?(Symbol)
+ result = instance.send(@method) ? 1 : 0
+ elsif @method.is_a?(Proc)
+ result = instance.instance_exec(&@method) ? 1 : 0
+ end
+ result
+ end
+
+ def dispatch_collection(instance)
+ result = instance.send(@method).count
+ end
+end
17 lib/scoring_ruleset/metaid.rb
@@ -0,0 +1,17 @@
+# Code by why_the_lucky_stiff
+
+class Object
+ # The hidden singleton lurks behind everyone
+ def metaclass; class << self; self; end; end
+ def meta_eval &blk; metaclass.instance_eval &blk; end
+
+ # Adds methods to a metaclass
+ def meta_def name, &blk
+ meta_eval { define_method name, &blk }
+ end
+
+ # Defines an instance method within a class
+ def class_def name, &blk
+ class_eval { define_method name, &blk }
+ end
+end
12 lib/scoring_ruleset/rule.rb
@@ -0,0 +1,12 @@
+class Rule
+ attr_accessor :points
+
+ def initialize(points, condition)
+ @points = points
+ @condition = condition
+ end
+
+ def evaluate(instance)
+ @condition.evaluate(instance) * @points
+ end
+end
50 lib/scoring_ruleset/ruleset.rb
@@ -0,0 +1,50 @@
+class Ruleset
+ def initialize
+ @rules = []
+ end
+
+ def add_points(criteria)
+ create_rule(criteria)
+ end
+
+ def remove_points(criteria)
+ create_rule(criteria, :multiplier => -1)
+ end
+
+ def evaluate(instance)
+ @rules.inject(0) do |memo, rule|
+ memo += rule.evaluate(instance)
+ end
+ end
+
+ private
+ def create_rule(criteria, options = {})
+ options[:multiplier] = 1 unless options[:multiplier]
+ points = criteria.delete(:points) || 1
+ validade_conditions(criteria)
+ condition = build_condition_from_criteria(criteria)
+ rule = Rule.new(points * options[:multiplier], condition)
+ add_rule(rule)
+ end
+
+ def build_condition_from_criteria(criteria)
+ condition_data = criteria.shift
+ Condition.new(condition_data[0], condition_data[1])
+ end
+
+ def validade_conditions(criteria)
+ valid_keys = [:if, :unless, :each]
+ keys = criteria.keys
+
+ raise(ArgumentError, "Each rule should have a condition") if keys.size == 0
+
+ unknown_keys = keys - [valid_keys].flatten
+ raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
+
+ raise(ArgumentError, "Each rule should have only one condition") if keys.size > 1
+ end
+
+ def add_rule(rule)
+ @rules << rule
+ end
+end
3  lib/scoring_ruleset/version.rb
@@ -0,0 +1,3 @@
+module ScoringRuleset
+ VERSION = "0.0.1"
+end
24 scoring_ruleset.gemspec
@@ -0,0 +1,24 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "scoring_ruleset/version"
+
+Gem::Specification.new do |s|
+ s.name = "scoring_ruleset"
+ s.version = ScoringRuleset::VERSION
+ s.platform = Gem::Platform::RUBY
+ s.authors = ["Lucas Húngaro"]
+ s.email = ["lucashungaro@gmail.com"]
+ s.homepage = "http://rubygems.org/gems/scoring_ruleset"
+ s.summary = %q{TODO: Write a gem summary}
+ s.description = %q{TODO: Write a gem description}
+
+ s.rubyforge_project = "scoring_ruleset"
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+
+ s.add_development_dependency(%q<rspec>, [">= 1.3.0"])
+ s.add_development_dependency(%q<mocha>, [">= 0.9.8"])
+end
19 spec/fixtures/my_model.rb
@@ -0,0 +1,19 @@
+class MyModel
+ def follower
+ r = OpenStruct.new
+ r.count = 300
+ r
+ end
+
+ def can_remove?
+ true
+ end
+
+ def age
+ 20
+ end
+
+ def is_new_user?
+ true
+ end
+end
33 spec/fixtures/rulesets.rb
@@ -0,0 +1,33 @@
+VALID_RULESET = <<-CODE
+scoring_ruleset do |rule|
+ rule.add_points :points => 10, :if => lambda {self.age >= 18}
+ rule.remove_points :points => 5, :if => :can_remove?
+ rule.add_points :points => 5, :unless => lambda {self.is_new_user?}
+ rule.add_points :points => 1, :each => :follower
+end
+CODE
+
+INVALID_RULESET_EMPTY = <<-CODE
+scoring_ruleset
+CODE
+
+INVALID_RULESET_MANY_CONDITIONS = <<-CODE
+scoring_ruleset do |rule|
+ rule.add_points :points => 10, :if => lambda {self.age >= 18}, :unless => lambda {true}
+ rule.remove_points :points => 5, :if => :can_remove?
+end
+CODE
+
+INVALID_RULESET_NO_CONDITIONS = <<-CODE
+scoring_ruleset do |rule|
+ rule.add_points :points => 10
+ rule.remove_points :points => 5, :if => :can_remove?
+end
+CODE
+
+INVALID_RULESET_NON_EXISTENT_CONDITIONS = <<-CODE
+scoring_ruleset do |rule|
+ rule.add_points :points => 10, :crazy => lambda {true}
+ rule.remove_points :points => 5, :if => :can_remove?
+end
+CODE
49 spec/scoring_ruleset_spec.rb
@@ -0,0 +1,49 @@
+require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
+require File.expand_path(File.dirname(__FILE__) + '/fixtures/my_model')
+require File.expand_path(File.dirname(__FILE__) + '/fixtures/rulesets')
+
+describe ScoringRuleset do
+ before(:each) do
+ MyModel.send(:include, ScoringRuleset)
+ end
+
+ after(:each) do
+ # reload the file to undefine the ruleset
+ load File.expand_path(File.dirname(__FILE__) + '/fixtures/my_model.rb')
+ end
+
+ context "contract" do
+ specify "ruleset needs a block with its rules" do
+ doing {
+ MyModel.class_eval {eval INVALID_RULESET_EMPTY}
+ }.should raise_exception(ArgumentError)
+ end
+
+ specify "every rule should accept only one condition" do
+ doing {
+ MyModel.class_eval {eval INVALID_RULESET_MANY_CONDITIONS}
+ }.should raise_exception(ArgumentError)
+ end
+
+ specify "every rule should have a condition" do
+ doing {
+ MyModel.class_eval {eval INVALID_RULESET_NO_CONDITIONS}
+ }.should raise_exception(ArgumentError)
+ end
+
+ specify "only accepted conditions are :if, :unless and :each" do
+ doing {
+ MyModel.class_eval {eval INVALID_RULESET_NON_EXISTENT_CONDITIONS}
+ }.should raise_exception(ArgumentError)
+ end
+ end
+
+ context "functionality" do
+ it "should correctly calculate the object score according to the ruleset" do
+ MyModel.class_eval {eval VALID_RULESET}
+ obj = MyModel.new
+
+ obj.calculate_score.should == 305
+ end
+ end
+end
1  spec/spec.opts
@@ -0,0 +1 @@
+--color
11 spec/spec_helper.rb
@@ -0,0 +1,11 @@
+$LOAD_PATH.unshift(File.dirname(__FILE__))
+$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+require 'scoring_ruleset'
+require 'spec'
+require 'spec/autorun'
+
+Spec::Runner.configure do |config|
+ config.mock_with :mocha
+end
+
+alias doing lambda
Please sign in to comment.
Something went wrong with that request. Please try again.