sapling 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 ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in sapling.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,12 @@
1
+ Sapling Gem
2
+ ===========
3
+
4
+ Sapling lets you seed your new features to just a few users at a time. You can change which and how many users are
5
+ seeded for a feature dynamically by updating the database via the Sapling API. Core features are the ability to
6
+ seed a feature for specific users and/or a percentage of users.
7
+
8
+ Heritage
9
+ --------
10
+
11
+ This is a port of the [rollout gem](https://github.com/jamesgolick/rollout) for
12
+ use with ActiveRecord instead of Redis. We dropped the groups functionality, but otherwise we mirrored the API.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new('spec') do |t|
7
+ t.verbose = true
8
+ end
9
+
10
+ task :default => :spec
data/db/create.sql ADDED
@@ -0,0 +1,6 @@
1
+ CREATE TABLE `sapling_settings` (
2
+ feature varchar(255),
3
+ percentage integer, -- 0 to 100 or NULL
4
+ user_id integer -- user's ID or NULL
5
+ );
6
+
data/lib/sapling.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "sapling/version"
2
+ require "sapling/util"
3
+ require "sapling/base"
4
+ require "sapling/memory"
5
+ require "sapling/active_record_model"
6
+ require "sapling/active_record"
7
+
8
+ module Sapling
9
+ # Your code goes here...
10
+ end
@@ -0,0 +1,53 @@
1
+ module Sapling
2
+ class ActiveRecord < Base
3
+
4
+ def table_name
5
+ Model.table_name
6
+ end
7
+
8
+ module ClientAPI
9
+ # see Sapling::API::Client
10
+ def active?(feature, options={})
11
+ options = Util.normalized_options options
12
+ v =Model.count(:conditions => [
13
+ "feature = ? AND ((user_id IS NOT NULL AND user_id = ?) OR (percentage IS NOT NULL AND ? < percentage)) ",
14
+ feature,
15
+ (u=options[:user]) && u.id,
16
+ ((c=options[:context_id]) && c%100) || 100
17
+ ])
18
+ v > 0
19
+ end
20
+ end
21
+ include ClientAPI
22
+
23
+ module AdminAPI
24
+
25
+ def activate_user(feature, user)
26
+ Model.transaction do
27
+ deactivate_user(feature, user)
28
+ Model.create(:feature => feature, :user_id => user.id)
29
+ end
30
+ end
31
+
32
+ def deactivate_user(feature, user)
33
+ Model.delete_all ["feature = ? AND percentage IS NULL and user_id = ?",feature,user.id]
34
+ end
35
+
36
+ def activate_percentage(feature, percentage)
37
+ raise "invalid percentage #{percentage.inspect}" unless percentage.kind_of?(Integer) && percentage>=0 && percentage<=100
38
+ Model.transaction do
39
+ deactivate_percentage(feature)
40
+ Model.create(:feature => feature, :percentage => percentage)
41
+ end
42
+ end
43
+
44
+ def deactivate_percentage(feature)
45
+ Model.delete_all ["feature = ? AND percentage IS NOT NULL AND user_id IS NULL",
46
+ feature
47
+ ]
48
+ end
49
+ end
50
+
51
+ include AdminAPI
52
+ end
53
+ end
@@ -0,0 +1,8 @@
1
+ require "active_record"
2
+ module Sapling
3
+ class ActiveRecord < Base
4
+ class Model < ::ActiveRecord::Base
5
+ set_table_name "sapling_settings"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ module Sapling::API
2
+ module Client
3
+ # options.kind_of? Hash
4
+ # :user => user
5
+ # :context_id => Integer
6
+ # options.kind_of? Integer
7
+ # {:context_id => options}
8
+ # else
9
+ # {:user => options}
10
+ def active?(feature, options={}) {}
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ module Sapling
2
+ class Base
3
+ end
4
+ end
@@ -0,0 +1,82 @@
1
+ module Sapling
2
+ class Memory < Base
3
+
4
+ class Feature
5
+ attr_accessor :users,:percentage
6
+
7
+ def initialize
8
+ @users={}
9
+ self.percentage = 0
10
+ end
11
+
12
+ # see Sapling::API::Client
13
+ def active?(options={})
14
+ options = Util::normalized_options(options)
15
+ individually_active?(options[:user]) || percentage_active?(options)
16
+ end
17
+
18
+ def percentage_active?(options={})
19
+ (Util.context_id(options) % 100) < percentage
20
+ end
21
+
22
+ def individually_active?(user)
23
+ user && users[user.id]
24
+ end
25
+
26
+ def activate_user(user)
27
+ users[user.id]=true
28
+ end
29
+
30
+ def deactivate_user(user)
31
+ users.delete(user.id)
32
+ end
33
+
34
+ def activate_percentage(percentage)
35
+ @percentage=percentage
36
+ end
37
+
38
+ def deactivate_percentage
39
+ @percentage=0
40
+ end
41
+ end
42
+
43
+ attr_accessor :features
44
+
45
+ def initialize
46
+ @features={}
47
+ end
48
+
49
+ module ClientAPI
50
+ # see Sapling::API::Client
51
+ def active?(feature, options={})
52
+ options = Util::normalized_options(options)
53
+ (f = @features[feature]) && f.active?(options)
54
+ end
55
+ end
56
+
57
+ module AdminAPI
58
+ def activate_feature(feature)
59
+ features[feature]||=Feature.new
60
+ end
61
+
62
+ def activate_user(feature, user)
63
+ activate_feature(feature).activate_user(user)
64
+ end
65
+
66
+ def deactivate_user(feature, user)
67
+ (f=features[feature]) && f.deactivate_user(user)
68
+ end
69
+
70
+ def activate_percentage(feature, percentage)
71
+ activate_feature(feature).activate_percentage(percentage)
72
+ end
73
+
74
+ def deactivate_percentage(feature)
75
+ activate_feature(feature).deactivate_percentage
76
+ end
77
+ end
78
+
79
+ include ClientAPI
80
+ include AdminAPI
81
+ end
82
+ end
@@ -0,0 +1,22 @@
1
+ module Sapling
2
+ class Util
3
+ class << self
4
+ def context_id(options)
5
+ options[:context_id] || options[:user].id
6
+ end
7
+
8
+ def normalized_options(options)
9
+ case options
10
+ when Hash then
11
+ options[:context_id] ||= options[:user].id
12
+ options
13
+ when Integer then
14
+ {:context_id => options}
15
+ else
16
+ user=options
17
+ {:user => user, :context_id => user.id}
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ module Sapling
2
+ VERSION = "0.0.1"
3
+ end
data/sapling.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "sapling/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "sapling"
7
+ s.version = Sapling::VERSION
8
+ s.authors = ["Shane Brinkman-Davis", "Jason Strutz"]
9
+ s.email = ["shanebdavis@imikimi.com", "jason@cumuluscode.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{A gem expressing if a feature is seeded for a user}
12
+ s.description = %q{}
13
+
14
+ s.rubyforge_project = "sapling"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ s.add_development_dependency "rspec", "~>2.7.0"
23
+ s.add_development_dependency "mocha"
24
+ s.add_development_dependency "activerecord", "~>3.1.1"
25
+ s.add_development_dependency "sqlite3", "~>1.3.4"
26
+ end
@@ -0,0 +1,42 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'active_record'
3
+
4
+ describe "Sapling::ActiveRecord" do
5
+ it_behaves_like Sapling do
6
+ before do
7
+ ActiveRecord::Base.establish_connection(
8
+ :adapter => 'sqlite3',
9
+ :database => ':memory:'
10
+ )
11
+ sql = File.read(File.expand_path(File.dirname(__FILE__) + '/../db/create.sql'))
12
+ ActiveRecord::Base.connection.execute sql
13
+ @sapling = Sapling::ActiveRecord.new
14
+ end
15
+
16
+ end
17
+ #
18
+ # it "should init" do
19
+ # Sapling::Memory.new
20
+ # end
21
+ #
22
+ # it "should support activating users" do
23
+ # mem = Sapling::Memory.new
24
+ # user = UserMock.new
25
+ #
26
+ # mem.active?(:my_feature, user).should be_false
27
+ #
28
+ # mem.activate_user(:my_feature, user)
29
+ # mem.active?(:my_feature, user).should be_true
30
+ # end
31
+ #
32
+ # it "should support deactivating users" do
33
+ # mem = Sapling::Memory.new
34
+ # user = UserMock.new
35
+ #
36
+ # mem.activate_user(:my_feature, user)
37
+ # mem.active?(:my_feature, user).should be_true
38
+ #
39
+ # mem.deactivate_user(:my_feature, user)
40
+ # mem.active?(:my_feature, user).should be_false
41
+ # end
42
+ end
@@ -0,0 +1,13 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Sapling::Memory::Feature" do
4
+ it "should support activating users" do
5
+ f=Sapling::Memory::Feature.new
6
+ u=UserMock.new
7
+
8
+ f.active?(u).should be_false
9
+ f.activate_user(u)
10
+
11
+ f.active?(u).should be_true
12
+ end
13
+ end
@@ -0,0 +1,35 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Sapling::Memory" do
4
+ it_behaves_like Sapling do
5
+ before do
6
+ @sapling = Sapling::Memory.new
7
+ end
8
+
9
+ end
10
+ #
11
+ # it "should init" do
12
+ # Sapling::Memory.new
13
+ # end
14
+ #
15
+ # it "should support activating users" do
16
+ # mem = Sapling::Memory.new
17
+ # user = UserMock.new
18
+ #
19
+ # mem.active?(:my_feature, user).should be_false
20
+ #
21
+ # mem.activate_user(:my_feature, user)
22
+ # mem.active?(:my_feature, user).should be_true
23
+ # end
24
+ #
25
+ # it "should support deactivating users" do
26
+ # mem = Sapling::Memory.new
27
+ # user = UserMock.new
28
+ #
29
+ # mem.activate_user(:my_feature, user)
30
+ # mem.active?(:my_feature, user).should be_true
31
+ #
32
+ # mem.deactivate_user(:my_feature, user)
33
+ # mem.active?(:my_feature, user).should be_false
34
+ # end
35
+ end
@@ -0,0 +1,86 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ shared_examples_for Sapling do
4
+
5
+ describe "features default to being disabled" do
6
+ it "is not active for a specific user by default" do
7
+ @sapling.should_not be_active(:chat, stub(:id => 5))
8
+ end
9
+ end
10
+
11
+ describe "activating a percentage" do
12
+ before do
13
+ @sapling.activate_percentage(:chat, 20)
14
+ end
15
+
16
+ it "activates the feature for that percentage of users" do
17
+ (1..1000).select { |id| @sapling.active?(:chat, UserMock.new(id)) }.length.should == 200
18
+ end
19
+ end
20
+
21
+ describe "deactivating a percentage" do
22
+ before do
23
+ @sapling.activate_percentage(:chat, 100)
24
+ @sapling.deactivate_percentage(:chat)
25
+ end
26
+
27
+ it "becomes inactive for all users" do
28
+ @sapling.should_not be_active(:chat, stub(:id => 24))
29
+ end
30
+ end
31
+
32
+ describe "activating a specific user" do
33
+ before do
34
+ @sapling.activate_user(:chat, stub(:id => 2))
35
+ end
36
+
37
+ it "activates the feature for that user" do
38
+ @sapling.should be_active(:chat, stub(:id => 2))
39
+ end
40
+ end
41
+
42
+ describe "deactivating a specific user" do
43
+ before do
44
+ @sapling.activate_user(:chat, stub(:id => 2))
45
+ @sapling.deactivate_user(:chat, stub(:id => 2))
46
+ end
47
+
48
+ it "deactivates the feature for that user" do
49
+ @sapling.should_not be_active(:chat, stub(:id => 2))
50
+ end
51
+ end
52
+
53
+ describe "activate percentage consider a generic integer" do
54
+ before do
55
+ @sapling.activate_percentage(:chat, 20)
56
+ end
57
+
58
+ it "activates a context_id" do
59
+ @sapling.should_not be_active(:chat, 60)
60
+ end
61
+ end
62
+
63
+ end
64
+ #
65
+ #
66
+ # feature :string
67
+ # user_id :integer
68
+ # group :string
69
+ # percentage :integer
70
+ #
71
+ #
72
+ # select count(*) from saplingawfwefwef where feature = 'aabce'
73
+ # and percentage < #{user_id % 100}
74
+ #
75
+ # select count(*) from saplingawfwefwef where feature = 'aabce'
76
+ # and group = 'admins'
77
+ #
78
+ # select count(*) from saplingawfwefwef where feature = 'aabce'
79
+ # and user_id = 123
80
+ #
81
+ # select feature from sapling where user_id = id or precentage < userid% 100 or group in (#{groups.join(',')})
82
+ #
83
+ # def current_features
84
+ # @features ||=
85
+ # end
86
+ #
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,19 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ require 'sapling'
5
+ require 'rspec'
6
+ require 'rspec/autorun'
7
+ require 'sapling_examples'
8
+
9
+ class UserMock
10
+ def initialize(i=1)
11
+ @id=i
12
+ end
13
+ def id;@id;end
14
+ end
15
+
16
+ RSpec.configure do |config|
17
+ # config.mock_with :mocha
18
+ config.before { }
19
+ end
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sapling
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Shane Brinkman-Davis
14
+ - Jason Strutz
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2011-12-02 00:00:00 -08:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: rspec
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ~>
29
+ - !ruby/object:Gem::Version
30
+ hash: 19
31
+ segments:
32
+ - 2
33
+ - 7
34
+ - 0
35
+ version: 2.7.0
36
+ type: :development
37
+ version_requirements: *id001
38
+ - !ruby/object:Gem::Dependency
39
+ name: mocha
40
+ prerelease: false
41
+ requirement: &id002 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ hash: 3
47
+ segments:
48
+ - 0
49
+ version: "0"
50
+ type: :development
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: activerecord
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ hash: 1
61
+ segments:
62
+ - 3
63
+ - 1
64
+ - 1
65
+ version: 3.1.1
66
+ type: :development
67
+ version_requirements: *id003
68
+ - !ruby/object:Gem::Dependency
69
+ name: sqlite3
70
+ prerelease: false
71
+ requirement: &id004 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ hash: 19
77
+ segments:
78
+ - 1
79
+ - 3
80
+ - 4
81
+ version: 1.3.4
82
+ type: :development
83
+ version_requirements: *id004
84
+ description: ""
85
+ email:
86
+ - shanebdavis@imikimi.com
87
+ - jason@cumuluscode.com
88
+ executables: []
89
+
90
+ extensions: []
91
+
92
+ extra_rdoc_files: []
93
+
94
+ files:
95
+ - .gitignore
96
+ - Gemfile
97
+ - README.md
98
+ - Rakefile
99
+ - db/create.sql
100
+ - lib/sapling.rb
101
+ - lib/sapling/active_record.rb
102
+ - lib/sapling/active_record_model.rb
103
+ - lib/sapling/api.rb
104
+ - lib/sapling/base.rb
105
+ - lib/sapling/memory.rb
106
+ - lib/sapling/util.rb
107
+ - lib/sapling/version.rb
108
+ - sapling.gemspec
109
+ - spec/active_record_spec.rb
110
+ - spec/memory_feature_spec.rb
111
+ - spec/memory_spec.rb
112
+ - spec/sapling_examples.rb
113
+ - spec/spec.opts
114
+ - spec/spec_helper.rb
115
+ has_rdoc: true
116
+ homepage: ""
117
+ licenses: []
118
+
119
+ post_install_message:
120
+ rdoc_options: []
121
+
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ none: false
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ hash: 3
130
+ segments:
131
+ - 0
132
+ version: "0"
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ none: false
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ hash: 3
139
+ segments:
140
+ - 0
141
+ version: "0"
142
+ requirements: []
143
+
144
+ rubyforge_project: sapling
145
+ rubygems_version: 1.5.3
146
+ signing_key:
147
+ specification_version: 3
148
+ summary: A gem expressing if a feature is seeded for a user
149
+ test_files:
150
+ - spec/active_record_spec.rb
151
+ - spec/memory_feature_spec.rb
152
+ - spec/memory_spec.rb
153
+ - spec/sapling_examples.rb
154
+ - spec/spec.opts
155
+ - spec/spec_helper.rb