sapling 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/.gitignore +2 -0
  2. data/lib/sapling.rb +1 -0
  3. data/lib/sapling/active_record.rb +32 -10
  4. data/lib/sapling/generators/css_generator.rb +31 -0
  5. data/lib/sapling/memory.rb +14 -7
  6. data/lib/sapling/rails.rb +17 -0
  7. data/lib/sapling/rails/action_controller.rb +23 -0
  8. data/lib/sapling/rails/controllers/sapling_controller.rb +7 -0
  9. data/lib/sapling/rails/railtie.rb +7 -0
  10. data/lib/sapling/rails/view_helpers.rb +22 -0
  11. data/lib/sapling/util.rb +9 -11
  12. data/lib/sapling/version.rb +1 -1
  13. data/rails/init.rb +2 -0
  14. data/sapling.gemspec +4 -3
  15. data/spec/active_record_spec.rb +0 -26
  16. data/spec/css_generator_spec.rb +47 -0
  17. data/spec/memory_feature_spec.rb +2 -2
  18. data/spec/memory_spec.rb +0 -26
  19. data/spec/rails_app/README +243 -0
  20. data/spec/rails_app/Rakefile +10 -0
  21. data/spec/rails_app/app/controllers/application_controller.rb +20 -0
  22. data/spec/rails_app/app/controllers/spaceman_spiffs_controller.rb +90 -0
  23. data/spec/rails_app/app/controllers/user_sessions_controller.rb +14 -0
  24. data/spec/rails_app/app/helpers/application_helper.rb +3 -0
  25. data/spec/rails_app/app/helpers/spaceman_spiffs_helper.rb +2 -0
  26. data/spec/rails_app/app/models/spaceman_spiff.rb +2 -0
  27. data/spec/rails_app/app/models/user.rb +3 -0
  28. data/spec/rails_app/app/views/layouts/spaceman_spiffs.html.erb +18 -0
  29. data/spec/rails_app/app/views/spaceman_spiffs/edit.html.erb +16 -0
  30. data/spec/rails_app/app/views/spaceman_spiffs/index.html.erb +21 -0
  31. data/spec/rails_app/app/views/spaceman_spiffs/multiple_features.html.erb +7 -0
  32. data/spec/rails_app/app/views/spaceman_spiffs/new.html.erb +15 -0
  33. data/spec/rails_app/app/views/spaceman_spiffs/show.html.erb +8 -0
  34. data/spec/rails_app/config/boot.rb +114 -0
  35. data/spec/rails_app/config/database.yml +17 -0
  36. data/spec/rails_app/config/environment.rb +48 -0
  37. data/spec/rails_app/config/environments/development.rb +17 -0
  38. data/spec/rails_app/config/environments/production.rb +28 -0
  39. data/spec/rails_app/config/environments/test.rb +28 -0
  40. data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  41. data/spec/rails_app/config/initializers/cookie_verification_secret.rb +7 -0
  42. data/spec/rails_app/config/initializers/inflections.rb +10 -0
  43. data/spec/rails_app/config/initializers/mime_types.rb +5 -0
  44. data/spec/rails_app/config/initializers/new_rails_defaults.rb +21 -0
  45. data/spec/rails_app/config/initializers/session_store.rb +15 -0
  46. data/spec/rails_app/config/locales/en.yml +5 -0
  47. data/spec/rails_app/config/routes.rb +48 -0
  48. data/spec/rails_app/db/migrate/20111207234232_create_spaceman_spiffs.rb +13 -0
  49. data/spec/rails_app/db/migrate/20111208003900_create_users.rb +13 -0
  50. data/spec/rails_app/db/migrate/20111208004048_add_sapling_settings.rb +13 -0
  51. data/spec/rails_app/db/schema.rb +32 -0
  52. data/spec/rails_app/db/seeds.rb +7 -0
  53. data/spec/rails_app/doc/README_FOR_APP +2 -0
  54. data/spec/rails_app/public/404.html +30 -0
  55. data/spec/rails_app/public/422.html +30 -0
  56. data/spec/rails_app/public/500.html +30 -0
  57. data/spec/rails_app/public/favicon.ico +0 -0
  58. data/spec/rails_app/public/images/rails.png +0 -0
  59. data/spec/rails_app/public/javascripts/application.js +2 -0
  60. data/spec/rails_app/public/javascripts/controls.js +963 -0
  61. data/spec/rails_app/public/javascripts/dragdrop.js +973 -0
  62. data/spec/rails_app/public/javascripts/effects.js +1128 -0
  63. data/spec/rails_app/public/javascripts/prototype.js +4320 -0
  64. data/spec/rails_app/public/robots.txt +5 -0
  65. data/spec/rails_app/public/stylesheets/scaffold.css +54 -0
  66. data/spec/rails_app/script/about +4 -0
  67. data/spec/rails_app/script/console +3 -0
  68. data/spec/rails_app/script/dbconsole +3 -0
  69. data/spec/rails_app/script/destroy +3 -0
  70. data/spec/rails_app/script/generate +3 -0
  71. data/spec/rails_app/script/performance/benchmarker +3 -0
  72. data/spec/rails_app/script/performance/profiler +3 -0
  73. data/spec/rails_app/script/plugin +3 -0
  74. data/spec/rails_app/script/runner +3 -0
  75. data/spec/rails_app/script/server +3 -0
  76. data/spec/rails_app/test/fixtures/sapling_settings.yml +9 -0
  77. data/spec/rails_app/test/fixtures/spaceman_spiffs.yml +7 -0
  78. data/spec/rails_app/test/fixtures/users.yml +7 -0
  79. data/spec/rails_app/test/integration/feature_test.rb +30 -0
  80. data/spec/rails_app/test/performance/browsing_test.rb +9 -0
  81. data/spec/rails_app/test/test_helper.rb +38 -0
  82. data/spec/rails_app/test/unit/helpers/spaceman_spiffs_helper_test.rb +4 -0
  83. data/spec/rails_app/test/unit/spaceman_spiff_test.rb +8 -0
  84. data/spec/rails_app/test/unit/user_test.rb +8 -0
  85. data/spec/sapling_examples.rb +88 -6
  86. metadata +181 -25
data/.gitignore CHANGED
@@ -3,3 +3,5 @@
3
3
  Gemfile.lock
4
4
  pkg/*
5
5
  .rvmrc
6
+ *.log
7
+ *.sqlite3
data/lib/sapling.rb CHANGED
@@ -4,6 +4,7 @@ require "sapling/base"
4
4
  require "sapling/memory"
5
5
  require "sapling/active_record_model"
6
6
  require "sapling/active_record"
7
+ require "sapling/generators/css_generator"
7
8
 
8
9
  module Sapling
9
10
  # Your code goes here...
@@ -5,18 +5,40 @@ module Sapling
5
5
  Model.table_name
6
6
  end
7
7
 
8
+ private
9
+ def query_conditions(feature,normalized_options={})
10
+ ret=[
11
+ "#{'feature = ? AND ' if feature} ((user_id IS NOT NULL AND user_id = ?) OR (percentage IS NOT NULL AND ? < percentage)) "
12
+ ]
13
+ ret<<feature.to_s if feature
14
+ ret+=[
15
+ normalized_options[:user_id],
16
+ Util::modded_context_id(normalized_options)
17
+ ]
18
+ ret
19
+ end
20
+ public
21
+
8
22
  module ClientAPI
9
23
  # see Sapling::API::Client
10
24
  def active?(feature, options={})
11
25
  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
- ])
26
+ v = Model.count(:conditions => query_conditions(feature,options))
18
27
  v > 0
19
28
  end
29
+
30
+ def features
31
+ features = Model.find :all, :select => "feature", :group => "feature", :order => "feature"
32
+ features.map {|record| record.feature.to_sym}
33
+ end
34
+
35
+ # returns a list of features enabled for a user
36
+ # see Sapling::API::Client
37
+ def active_features(options={})
38
+ options = Util.normalized_options options
39
+ features = Model.find :all, :conditions => query_conditions(nil,options)
40
+ features.map {|record| record.feature.to_sym}.uniq
41
+ end
20
42
  end
21
43
  include ClientAPI
22
44
 
@@ -25,25 +47,25 @@ module Sapling
25
47
  def activate_user(feature, user)
26
48
  Model.transaction do
27
49
  deactivate_user(feature, user)
28
- Model.create(:feature => feature, :user_id => user.id)
50
+ Model.create(:feature => feature.to_s, :user_id => user.id)
29
51
  end
30
52
  end
31
53
 
32
54
  def deactivate_user(feature, user)
33
- Model.delete_all ["feature = ? AND percentage IS NULL and user_id = ?",feature,user.id]
55
+ Model.delete_all ["feature = ? AND percentage IS NULL and user_id = ?",feature.to_s,user.id]
34
56
  end
35
57
 
36
58
  def activate_percentage(feature, percentage)
37
59
  raise "invalid percentage #{percentage.inspect}" unless percentage.kind_of?(Integer) && percentage>=0 && percentage<=100
38
60
  Model.transaction do
39
61
  deactivate_percentage(feature)
40
- Model.create(:feature => feature, :percentage => percentage)
62
+ Model.create(:feature => feature.to_s, :percentage => percentage)
41
63
  end
42
64
  end
43
65
 
44
66
  def deactivate_percentage(feature)
45
67
  Model.delete_all ["feature = ? AND percentage IS NOT NULL AND user_id IS NULL",
46
- feature
68
+ feature.to_s
47
69
  ]
48
70
  end
49
71
  end
@@ -0,0 +1,31 @@
1
+ require "set"
2
+
3
+ module Sapling
4
+ class CssGenerator
5
+ attr_accessor :sapling
6
+
7
+ def initialize(sapling)
8
+ @sapling=sapling
9
+ end
10
+
11
+ def prefix
12
+ "sapling_css"
13
+ end
14
+
15
+ def css_class(feature, on)
16
+ "#{prefix}_#{feature.to_s}_#{on ? 'on' : 'off'}"
17
+ end
18
+
19
+ # see Sapling::API::Client for options
20
+ def to_s(options={})
21
+ features = Set.new @sapling.features
22
+ active_features = Set.new @sapling.active_features options
23
+ inactive_features = features - active_features
24
+
25
+ [
26
+ active_features.collect {|f|".#{css_class(f, false)} { display:none !important; }"},
27
+ inactive_features.collect {|f|".#{css_class(f, true)} { display:none !important; }"},
28
+ ].flatten.join("\n")
29
+ end
30
+ end
31
+ end
@@ -11,12 +11,12 @@ module Sapling
11
11
 
12
12
  # see Sapling::API::Client
13
13
  def active?(options={})
14
- options = Util::normalized_options(options)
14
+ options = Util.normalized_options(options)
15
15
  individually_active?(options[:user]) || percentage_active?(options)
16
16
  end
17
17
 
18
18
  def percentage_active?(options={})
19
- (Util.context_id(options) % 100) < percentage
19
+ (Util.modded_context_id(options)) < percentage
20
20
  end
21
21
 
22
22
  def individually_active?(user)
@@ -40,8 +40,6 @@ module Sapling
40
40
  end
41
41
  end
42
42
 
43
- attr_accessor :features
44
-
45
43
  def initialize
46
44
  @features={}
47
45
  end
@@ -49,14 +47,23 @@ module Sapling
49
47
  module ClientAPI
50
48
  # see Sapling::API::Client
51
49
  def active?(feature, options={})
52
- options = Util::normalized_options(options)
50
+ options = Util.normalized_options(options)
53
51
  (f = @features[feature]) && f.active?(options)
54
52
  end
53
+
54
+ def features
55
+ @features.keys.sort_by {|k| k.to_s}
56
+ end
57
+
58
+ # see Sapling::API::Client
59
+ def active_features(options={})
60
+ features.select {|feature| active?(feature,options)}
61
+ end
55
62
  end
56
63
 
57
64
  module AdminAPI
58
65
  def activate_feature(feature)
59
- features[feature]||=Feature.new
66
+ @features[feature]||=Feature.new
60
67
  end
61
68
 
62
69
  def activate_user(feature, user)
@@ -64,7 +71,7 @@ module Sapling
64
71
  end
65
72
 
66
73
  def deactivate_user(feature, user)
67
- (f=features[feature]) && f.deactivate_user(user)
74
+ (f=@features[feature]) && f.deactivate_user(user)
68
75
  end
69
76
 
70
77
  def activate_percentage(feature, percentage)
@@ -0,0 +1,17 @@
1
+ require 'sapling/rails/action_controller'
2
+ require 'sapling/rails/view_helpers'
3
+
4
+ ActiveSupport::Dependencies.autoload_paths << File.expand_path(File.join(File.dirname(__FILE__), "rails", "controllers"))
5
+
6
+ ActionController::Routing::Routes.draw do |map|
7
+ map.sapling_stylesheet 'sapling/stylesheet.css', :controller => 'sapling', :action => 'stylesheet'
8
+ end
9
+
10
+ if defined?(Rails)
11
+ if Rails.version =~ /^3\./
12
+ # Haven't tested this
13
+ require 'sapling/rails/railtie'
14
+ else
15
+ ActionView::Base.send :include, Sapling::ViewHelpers
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ require 'action_controller'
2
+
3
+ module Sapling::ActionControllerExt
4
+ def feature_active?(feature, options={})
5
+ options[:user] ||= current_user
6
+ sapling.active?(feature, options)
7
+ end
8
+
9
+
10
+ def sapling
11
+ @@sapling ||= Sapling::ActiveRecord.new
12
+ end
13
+
14
+ def sapling_css_generator
15
+ @@sapling_css_generator ||= Sapling::CssGenerator.new(sapling)
16
+ end
17
+
18
+ end
19
+
20
+ class ActionController::Base
21
+ include Sapling::ActionControllerExt
22
+ helper_method :feature_active?, :sapling, :sapling_css_generator
23
+ end
@@ -0,0 +1,7 @@
1
+ class SaplingController < ApplicationController
2
+ def stylesheet
3
+ styles = sapling_css_generator.to_s(:user => current_user, :context_id => (request.session_options[:id] if request.session))
4
+ # styles = "boo"
5
+ render :text => styles, :content_type => 'text/css'
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Sapling
2
+ class Railtie < Rails::Railtie
3
+ initializer "sapling.view_helpers" do
4
+ ActionView::Base.send :include, ViewHelpers
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ module Sapling
2
+ module ViewHelpers
3
+
4
+ # include ActionView::Helpers::CaptureHelper
5
+ def feature_on(feature, &block)
6
+ feature_block(sapling_css_generator.css_class(feature, "style=\"display:none;\"", true), &block)
7
+ end
8
+
9
+ def feature_off(feature, &block)
10
+ feature_block(sapling_css_generator.css_class(feature, nil, false), &block)
11
+ end
12
+
13
+ private
14
+ def feature_block(css_class, params, &block)
15
+ concat "<span class=\"#{css_class}\" #{params}>"
16
+ block.call
17
+ concat "</span>"
18
+ end
19
+
20
+ end
21
+ end
22
+
data/lib/sapling/util.rb CHANGED
@@ -1,21 +1,19 @@
1
1
  module Sapling
2
2
  class Util
3
+ CONTEXT_ID_ONLY_ENABLED_IF_100_PERCENT_ENABLED = 99
4
+
3
5
  class << self
4
6
  def context_id(options)
5
- options[:context_id] || options[:user].id
7
+ options[:context_id] || ((u=options[:user]) && u.id)
8
+ end
9
+ def modded_context_id(options)
10
+ ((cid=context_id(options)) && (cid%100)) || CONTEXT_ID_ONLY_ENABLED_IF_100_PERCENT_ENABLED
6
11
  end
7
12
 
8
13
  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
14
+ options[:user_id] ||= options[:user].id if options[:user]
15
+ options[:context_id] ||= options[:user_id]
16
+ options
19
17
  end
20
18
  end
21
19
  end
@@ -1,3 +1,3 @@
1
1
  module Sapling
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
data/rails/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'sapling'
2
+ require 'sapling/rails'
data/sapling.gemspec CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |s|
8
8
  s.authors = ["Shane Brinkman-Davis", "Jason Strutz"]
9
9
  s.email = ["shanebdavis@imikimi.com", "jason@cumuluscode.com"]
10
10
  s.homepage = ""
11
- s.summary = %q{A gem expressing if a feature is seeded for a user}
12
- s.description = %q{}
11
+ s.summary = %q{Incrementally roll out your features. Uses ActiveRecord to store configuration and supports client-side roll-out of cached pages.}
12
+ s.description = %q{Sapling lets you seed your new features to just a few users at a time. You can change which and how many users are seeded for a feature dynamically by updating the database via the Sapling API. Core features are the ability to seed a feature for specific users and/or a percentage of users.}
13
13
 
14
14
  s.rubyforge_project = "sapling"
15
15
 
@@ -19,8 +19,9 @@ Gem::Specification.new do |s|
19
19
  s.require_paths = ["lib"]
20
20
 
21
21
  # specify any dependencies here; for example:
22
+ s.add_runtime_dependency "activerecord", ">2.3"
22
23
  s.add_development_dependency "rspec", "~>2.7.0"
24
+ s.add_development_dependency "rails", "2.3.12"
23
25
  s.add_development_dependency "mocha"
24
- s.add_development_dependency "activerecord", "~>3.1.1"
25
26
  s.add_development_dependency "sqlite3", "~>1.3.4"
26
27
  end
@@ -12,31 +12,5 @@ describe "Sapling::ActiveRecord" do
12
12
  ActiveRecord::Base.connection.execute sql
13
13
  @sapling = Sapling::ActiveRecord.new
14
14
  end
15
-
16
15
  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
16
  end
@@ -0,0 +1,47 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Sapling::CssGenerator" do
4
+
5
+ before do
6
+ ActiveRecord::Base.establish_connection(
7
+ :adapter => 'sqlite3',
8
+ :database => ':memory:'
9
+ )
10
+ sql = File.read(File.expand_path(File.dirname(__FILE__) + '/../db/create.sql'))
11
+ ActiveRecord::Base.connection.execute sql
12
+ @sapling = Sapling::ActiveRecord.new
13
+ end
14
+
15
+ describe "creating basic css" do
16
+ before do
17
+ @sapling.activate_user(:chat, stub(:id => 1))
18
+ end
19
+
20
+ it "outputs css" do
21
+ Sapling::CssGenerator.new(@sapling).to_s(:user => stub(:id => 1)).should == ".sapling_css_chat_off { display:none !important; }"
22
+ end
23
+ end
24
+
25
+ describe "creating more complex css" do
26
+ before do
27
+ @sapling.activate_percentage(:bicycle, 10)
28
+ @sapling.activate_user(:chat, stub(:id => 115))
29
+ @sapling.activate_user(:pwn, stub(:id => 102))
30
+ @sapling.activate_user(:juggle, stub(:id => 115))
31
+ end
32
+
33
+ it "test user bicycle & pwn user" do
34
+ output = Sapling::CssGenerator.new(@sapling).to_s(:user => stub(:id => 102))
35
+ %w{ bicycle_off chat_on juggle_on pwn_off }.each do |key|
36
+ output.should =~ /#{key}[^}]+?display:none/
37
+ end
38
+ end
39
+
40
+ it "test chat & juggle user" do
41
+ output = Sapling::CssGenerator.new(@sapling).to_s(:user => stub(:id => 115))
42
+ %w{ bicycle_on chat_off juggle_off pwn_on }.each do |key|
43
+ output.should =~ /#{key}[^}]+?display:none/
44
+ end
45
+ end
46
+ end
47
+ end
@@ -5,9 +5,9 @@ describe "Sapling::Memory::Feature" do
5
5
  f=Sapling::Memory::Feature.new
6
6
  u=UserMock.new
7
7
 
8
- f.active?(u).should be_false
8
+ f.active?(:user=>u).should be_false
9
9
  f.activate_user(u)
10
10
 
11
- f.active?(u).should be_true
11
+ f.active?(:user=>u).should be_true
12
12
  end
13
13
  end
data/spec/memory_spec.rb CHANGED
@@ -5,31 +5,5 @@ describe "Sapling::Memory" do
5
5
  before do
6
6
  @sapling = Sapling::Memory.new
7
7
  end
8
-
9
8
  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
9
  end