scoring_rules 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in scoring_rules.gemspec
4
+ gemspec
@@ -0,0 +1,80 @@
1
+ h1. scoring_rules
2
+
3
+ A small library that provides a simple DSL for creating scoring rules (for rankings, rating and similar uses).
4
+
5
+ Does scoring_rules helps your daily work with Rails? So, "please recommend me":http://workingwithrails.com/recommendation/new/person/9370-lucas-h-ngaro on Working With Rails. Thanks for your kindness! :)
6
+
7
+ h2. Why?
8
+
9
+ This kind of feature could by implemented without a DSL, but this really helps to build a transparent representation of business rules that is self-contained and easier to modify. Also, even if not a primary goal, this way of representation is easier for non-tech people to understand.
10
+
11
+ h2. How?
12
+
13
+ First, install the gem:
14
+
15
+ <pre>
16
+ $ [sudo] gem install scoring_rules
17
+ </pre>
18
+
19
+ Then, add it as a dependency in your code using your favorite way (a simple require or mechanisms like the Bundler gem).
20
+
21
+ The gem will provide you a module to mixin into your classes.
22
+
23
+ <pre>
24
+ class User
25
+ include ScoringRules
26
+
27
+ scoring_rules do |rule|
28
+ rule.add_points 10, :if => lambda {self.age >= 18} # adds 10
29
+ rule.remove_points 5, :if => :can_remove? # removes 5
30
+ rule.add_points 5, :unless => lambda {self.is_new_user?} # does nothing
31
+ rule.add_points 1, :each => :followers # adds 300
32
+ end
33
+
34
+ def followers
35
+ r = OpenStruct.new
36
+ r.count = 300
37
+ r
38
+ end
39
+
40
+ def can_remove?
41
+ true
42
+ end
43
+
44
+ def age
45
+ 20
46
+ end
47
+
48
+ def is_new_user?
49
+ true
50
+ end
51
+ end
52
+ </pre>
53
+
54
+ Each rule requires one condition, expressed through :if, :unless or :each. For now you can't supply more than one condition per rule (it was a goal, but proved unnecessary).
55
+
56
+ And to calculate the score, it's really simple:
57
+
58
+ <pre>
59
+ > user = User.get_me_some_user_from_somewhere
60
+ > user.calculate_score
61
+ => 305
62
+ </pre>
63
+
64
+ h2. Note on Patches/Pull Requests
65
+
66
+ * Fork the project.
67
+ * Make your feature addition or bug fix.
68
+ * Add tests for it. This is important so I don't break it in a
69
+ future version unintentionally.
70
+ * Commit, do not mess with rakefile, version, or history.
71
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
72
+ * Send me a pull request. Bonus points for topic branches.
73
+
74
+ h2. Contributors
75
+
76
+ Be a contributor! :)
77
+
78
+ h3. Copyright
79
+
80
+ Copyright (c) 2010 Lucas HĂșngaro. See LICENSE for details.
@@ -0,0 +1,10 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'spec/rake/spectask'
5
+ Spec::Rake::SpecTask.new(:spec) do |spec|
6
+ spec.libs << 'lib' << 'spec'
7
+ spec.spec_files = FileList['spec/**/*_spec.rb']
8
+ end
9
+
10
+ task :default => :spec
@@ -0,0 +1,27 @@
1
+ require "scoring_rules/metaid"
2
+ require "scoring_rules/ruleset"
3
+ require "scoring_rules/rule"
4
+ require "scoring_rules/condition"
5
+
6
+ module ScoringRules
7
+ module ClassMethods
8
+ def scoring_rules
9
+ raise(ArgumentError, "You must supply a block with the rules") unless block_given?
10
+ self.ruleset = Ruleset.new
11
+ yield self.ruleset
12
+ end
13
+ end
14
+
15
+ module InstanceMethods
16
+ def calculate_score
17
+ self.class.ruleset.evaluate(self)
18
+ end
19
+ end
20
+
21
+ def self.included(receiver)
22
+ receiver.extend ClassMethods
23
+ receiver.send :include, InstanceMethods
24
+
25
+ receiver.meta_eval { attr_accessor :ruleset }
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ class Condition
2
+ def initialize(type, method)
3
+ @type = type
4
+ @method = method
5
+ end
6
+
7
+ def evaluate(instance)
8
+ case @type
9
+ when :if
10
+ dispatch_pre_condition(instance)
11
+ when :unless
12
+ (dispatch_pre_condition(instance) - 1) * -1
13
+ when :each
14
+ dispatch_collection(instance)
15
+ end
16
+ end
17
+
18
+ private
19
+ def dispatch_pre_condition(instance)
20
+ result = 0
21
+ if @method.is_a?(Symbol)
22
+ result = instance.send(@method) ? 1 : 0
23
+ elsif @method.is_a?(Proc)
24
+ result = instance.instance_exec(&@method) ? 1 : 0
25
+ end
26
+ result
27
+ end
28
+
29
+ def dispatch_collection(instance)
30
+ result = instance.send(@method).count
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ # Code by why_the_lucky_stiff
2
+
3
+ class Object
4
+ # The hidden singleton lurks behind everyone
5
+ def metaclass; class << self; self; end; end
6
+ def meta_eval &blk; metaclass.instance_eval &blk; end
7
+
8
+ # Adds methods to a metaclass
9
+ def meta_def name, &blk
10
+ meta_eval { define_method name, &blk }
11
+ end
12
+
13
+ # Defines an instance method within a class
14
+ def class_def name, &blk
15
+ class_eval { define_method name, &blk }
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ class Rule
2
+ attr_accessor :points
3
+
4
+ def initialize(points, condition)
5
+ @points = points
6
+ @condition = condition
7
+ end
8
+
9
+ def evaluate(instance)
10
+ @condition.evaluate(instance) * @points
11
+ end
12
+ end
@@ -0,0 +1,48 @@
1
+ class Ruleset
2
+ def initialize
3
+ @rules = []
4
+ end
5
+
6
+ def add_points(points, criteria)
7
+ add_rule create_rule(points, criteria)
8
+ end
9
+
10
+ def remove_points(points, criteria)
11
+ add_rule create_rule(points, criteria, :multiplier => -1)
12
+ end
13
+
14
+ def evaluate(instance)
15
+ @rules.inject(0) do |memo, rule|
16
+ memo += rule.evaluate(instance)
17
+ end
18
+ end
19
+
20
+ private
21
+ def create_rule(points, criteria, options = {})
22
+ options[:multiplier] = 1 unless options[:multiplier]
23
+ validade_conditions(criteria)
24
+ condition = build_condition_from_criteria(criteria)
25
+ rule = Rule.new(points * options[:multiplier], condition)
26
+ end
27
+
28
+ def build_condition_from_criteria(criteria)
29
+ condition_data = criteria.shift
30
+ Condition.new(condition_data[0], condition_data[1])
31
+ end
32
+
33
+ def validade_conditions(criteria)
34
+ valid_keys = [:if, :unless, :each]
35
+ keys = criteria.keys
36
+
37
+ raise(ArgumentError, "Each rule should have a condition") if keys.size == 0
38
+
39
+ unknown_keys = keys - [valid_keys].flatten
40
+ raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
41
+
42
+ raise(ArgumentError, "Each rule should have only one condition") if keys.size > 1
43
+ end
44
+
45
+ def add_rule(rule)
46
+ @rules << rule
47
+ end
48
+ end
@@ -0,0 +1,3 @@
1
+ module ScoringRules
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "scoring_rules/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "scoring_rules"
7
+ s.version = ScoringRules::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Lucas HĂșngaro"]
10
+ s.email = ["lucashungaro@gmail.com"]
11
+ s.homepage = "http://rubygems.org/gems/scoring_rules"
12
+ s.summary = %q{A small library that provides a simple DSL for creating scoring rules (for rankings, rating and similar uses).}
13
+ s.description = %q{A small library that provides a simple DSL for creating scoring rules (for rankings, rating and similar uses).}
14
+
15
+ s.rubyforge_project = "scoring_rules"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_development_dependency(%q<rspec>, [">= 1.3.0"])
23
+ s.add_development_dependency(%q<mocha>, [">= 0.9.8"])
24
+ end
@@ -0,0 +1,19 @@
1
+ class MyModel
2
+ def followers
3
+ r = OpenStruct.new
4
+ r.count = 300
5
+ r
6
+ end
7
+
8
+ def can_remove?
9
+ true
10
+ end
11
+
12
+ def age
13
+ 20
14
+ end
15
+
16
+ def is_new_user?
17
+ true
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ VALID_RULESET = <<-CODE
2
+ scoring_rules do |rule|
3
+ rule.add_points 10, :if => lambda {self.age >= 18}
4
+ rule.remove_points 5, :if => :can_remove?
5
+ rule.add_points 5, :unless => lambda {self.is_new_user?}
6
+ rule.add_points 1, :each => :followers
7
+ end
8
+ CODE
9
+
10
+ INVALID_RULESET_EMPTY = <<-CODE
11
+ scoring_rules
12
+ CODE
13
+
14
+ INVALID_RULESET_MANY_CONDITIONS = <<-CODE
15
+ scoring_rules do |rule|
16
+ rule.add_points 10, :if => lambda {self.age >= 18}, :unless => lambda {true}
17
+ rule.remove_points 5, :if => :can_remove?
18
+ end
19
+ CODE
20
+
21
+ INVALID_RULESET_NO_CONDITIONS = <<-CODE
22
+ scoring_rules do |rule|
23
+ rule.add_points 10
24
+ rule.remove_points 5, :if => :can_remove?
25
+ end
26
+ CODE
27
+
28
+ INVALID_RULESET_NON_EXISTENT_CONDITIONS = <<-CODE
29
+ scoring_rules do |rule|
30
+ rule.add_points 10, :crazy => lambda {true}
31
+ rule.remove_points 5, :if => :can_remove?
32
+ end
33
+ CODE
@@ -0,0 +1,49 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require File.expand_path(File.dirname(__FILE__) + '/fixtures/my_model')
3
+ require File.expand_path(File.dirname(__FILE__) + '/fixtures/rulesets')
4
+
5
+ describe ScoringRules do
6
+ before(:each) do
7
+ MyModel.send(:include, ScoringRules)
8
+ end
9
+
10
+ after(:each) do
11
+ # reload the file to undefine the rule set
12
+ load File.expand_path(File.dirname(__FILE__) + '/fixtures/my_model.rb')
13
+ end
14
+
15
+ context "contract" do
16
+ specify "rule set needs a block with its rules" do
17
+ doing {
18
+ MyModel.class_eval {eval INVALID_RULESET_EMPTY}
19
+ }.should raise_exception(ArgumentError)
20
+ end
21
+
22
+ specify "every rule should accept only one condition" do
23
+ doing {
24
+ MyModel.class_eval {eval INVALID_RULESET_MANY_CONDITIONS}
25
+ }.should raise_exception(ArgumentError)
26
+ end
27
+
28
+ specify "every rule should have a condition" do
29
+ doing {
30
+ MyModel.class_eval {eval INVALID_RULESET_NO_CONDITIONS}
31
+ }.should raise_exception(ArgumentError)
32
+ end
33
+
34
+ specify "only accepted conditions are :if, :unless and :each" do
35
+ doing {
36
+ MyModel.class_eval {eval INVALID_RULESET_NON_EXISTENT_CONDITIONS}
37
+ }.should raise_exception(ArgumentError)
38
+ end
39
+ end
40
+
41
+ context "functionality" do
42
+ it "should correctly calculate the object score according to the rule set" do
43
+ MyModel.class_eval {eval VALID_RULESET}
44
+ obj = MyModel.new
45
+
46
+ obj.calculate_score.should == 305
47
+ end
48
+ end
49
+ end
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,11 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'scoring_rules'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ Spec::Runner.configure do |config|
8
+ config.mock_with :mocha
9
+ end
10
+
11
+ alias doing lambda
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scoring_rules
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - "Lucas H\xC3\xBAngaro"
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-11-08 00:00:00 -02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 27
30
+ segments:
31
+ - 1
32
+ - 3
33
+ - 0
34
+ version: 1.3.0
35
+ type: :development
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: mocha
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 43
46
+ segments:
47
+ - 0
48
+ - 9
49
+ - 8
50
+ version: 0.9.8
51
+ type: :development
52
+ version_requirements: *id002
53
+ description: A small library that provides a simple DSL for creating scoring rules (for rankings, rating and similar uses).
54
+ email:
55
+ - lucashungaro@gmail.com
56
+ executables: []
57
+
58
+ extensions: []
59
+
60
+ extra_rdoc_files: []
61
+
62
+ files:
63
+ - .gitignore
64
+ - Gemfile
65
+ - README.textile
66
+ - Rakefile
67
+ - lib/scoring_rules.rb
68
+ - lib/scoring_rules/condition.rb
69
+ - lib/scoring_rules/metaid.rb
70
+ - lib/scoring_rules/rule.rb
71
+ - lib/scoring_rules/ruleset.rb
72
+ - lib/scoring_rules/version.rb
73
+ - scoring_rules.gemspec
74
+ - spec/fixtures/my_model.rb
75
+ - spec/fixtures/rulesets.rb
76
+ - spec/scoring_rules_spec.rb
77
+ - spec/spec.opts
78
+ - spec/spec_helper.rb
79
+ has_rdoc: true
80
+ homepage: http://rubygems.org/gems/scoring_rules
81
+ licenses: []
82
+
83
+ post_install_message:
84
+ rdoc_options: []
85
+
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ hash: 3
94
+ segments:
95
+ - 0
96
+ version: "0"
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ hash: 3
103
+ segments:
104
+ - 0
105
+ version: "0"
106
+ requirements: []
107
+
108
+ rubyforge_project: scoring_rules
109
+ rubygems_version: 1.3.7
110
+ signing_key:
111
+ specification_version: 3
112
+ summary: A small library that provides a simple DSL for creating scoring rules (for rankings, rating and similar uses).
113
+ test_files:
114
+ - spec/fixtures/my_model.rb
115
+ - spec/fixtures/rulesets.rb
116
+ - spec/scoring_rules_spec.rb
117
+ - spec/spec.opts
118
+ - spec/spec_helper.rb