sapling 0.0.1 → 0.1.0

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.
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