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.
- data/.gitignore +3 -0
- data/Gemfile +4 -0
- data/README.textile +80 -0
- data/Rakefile +10 -0
- data/lib/scoring_rules.rb +27 -0
- data/lib/scoring_rules/condition.rb +32 -0
- data/lib/scoring_rules/metaid.rb +17 -0
- data/lib/scoring_rules/rule.rb +12 -0
- data/lib/scoring_rules/ruleset.rb +48 -0
- data/lib/scoring_rules/version.rb +3 -0
- data/scoring_rules.gemspec +24 -0
- data/spec/fixtures/my_model.rb +19 -0
- data/spec/fixtures/rulesets.rb +33 -0
- data/spec/scoring_rules_spec.rb +49 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +11 -0
- metadata +118 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.textile
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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,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,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,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
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -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
|