flipper 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +20 -0
  3. data/Guardfile +18 -0
  4. data/LICENSE +22 -0
  5. data/README.md +182 -0
  6. data/Rakefile +7 -0
  7. data/examples/basic.rb +27 -0
  8. data/examples/dsl.rb +85 -0
  9. data/examples/example_setup.rb +8 -0
  10. data/examples/group.rb +36 -0
  11. data/examples/individual_actor.rb +29 -0
  12. data/examples/percentage_of_actors.rb +35 -0
  13. data/examples/percentage_of_random.rb +33 -0
  14. data/flipper.gemspec +17 -0
  15. data/lib/flipper.rb +33 -0
  16. data/lib/flipper/adapters/memory.rb +35 -0
  17. data/lib/flipper/dsl.rb +61 -0
  18. data/lib/flipper/errors.rb +11 -0
  19. data/lib/flipper/feature.rb +49 -0
  20. data/lib/flipper/gate.rb +55 -0
  21. data/lib/flipper/gates/actor.rb +29 -0
  22. data/lib/flipper/gates/boolean.rb +29 -0
  23. data/lib/flipper/gates/group.rb +32 -0
  24. data/lib/flipper/gates/percentage_of_actors.rb +25 -0
  25. data/lib/flipper/gates/percentage_of_random.rb +25 -0
  26. data/lib/flipper/registry.rb +36 -0
  27. data/lib/flipper/spec/shared_adapter_specs.rb +78 -0
  28. data/lib/flipper/toggle.rb +31 -0
  29. data/lib/flipper/toggles/boolean.rb +22 -0
  30. data/lib/flipper/toggles/set.rb +17 -0
  31. data/lib/flipper/toggles/value.rb +17 -0
  32. data/lib/flipper/type.rb +18 -0
  33. data/lib/flipper/types/actor.rb +17 -0
  34. data/lib/flipper/types/boolean.rb +13 -0
  35. data/lib/flipper/types/group.rb +22 -0
  36. data/lib/flipper/types/percentage.rb +19 -0
  37. data/lib/flipper/types/percentage_of_actors.rb +6 -0
  38. data/lib/flipper/types/percentage_of_random.rb +6 -0
  39. data/lib/flipper/version.rb +3 -0
  40. data/spec/flipper/adapters/memory_spec.rb +19 -0
  41. data/spec/flipper/dsl_spec.rb +185 -0
  42. data/spec/flipper/feature_spec.rb +401 -0
  43. data/spec/flipper/registry_spec.rb +71 -0
  44. data/spec/flipper/types/actor_spec.rb +23 -0
  45. data/spec/flipper/types/boolean_spec.rb +9 -0
  46. data/spec/flipper/types/group_spec.rb +32 -0
  47. data/spec/flipper/types/percentage_of_actors_spec.rb +6 -0
  48. data/spec/flipper/types/percentage_of_random_spec.rb +6 -0
  49. data/spec/flipper/types/percentage_spec.rb +6 -0
  50. data/spec/flipper_spec.rb +59 -0
  51. data/spec/helper.rb +47 -0
  52. metadata +114 -0
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ log
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'rake'
5
+
6
+ group(:guard) do
7
+ gem 'guard'
8
+ gem 'guard-rspec'
9
+ gem 'guard-bundler'
10
+ gem 'growl'
11
+ end
12
+
13
+ group(:test) do
14
+ gem 'rspec'
15
+ gem 'timecop'
16
+ gem 'bson_ext'
17
+ gem 'mongo'
18
+ gem 'redis'
19
+ end
20
+
data/Guardfile ADDED
@@ -0,0 +1,18 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'bundler' do
5
+ watch('Gemfile')
6
+ watch(/^.+\.gemspec/)
7
+ end
8
+
9
+ rspec_options = {
10
+ :all_after_pass => false,
11
+ :all_on_start => false,
12
+ }
13
+
14
+ guard 'rspec', rspec_options do
15
+ watch(%r{^spec/.+_spec\.rb$}) { "spec" }
16
+ watch(%r{^lib/(.+)\.rb$}) { "spec" }
17
+ watch('spec/helper.rb') { "spec" }
18
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 John Nunemaker
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # Flipper
2
+
3
+ Feature flipping is the act of enabling or disabling features or parts of your application, ideally without re-deploying or changing anything in your code base.
4
+
5
+ The goal of this gem is to make turning features on or off so easy that everyone does it. Whatever your data store, throughput, or experience, feature flipping should be easy and relatively no extra burden to your application.
6
+
7
+ ## Why not use <insert gem name here, most likely rollout>?
8
+
9
+ I've used rollout extensively in the past and it was fantastic. The main reason I reinvented the wheel to some extent is:
10
+
11
+ * API - For whatever reason, I could never remember the API for rollout.
12
+ * Adapter Based - Rather than force redis, if you can implement a few simple methods, you can use the data store of your choice to power your flippers (memory, file system, mongo, redis, sql, etc.). It is also dead simple to front your data store with memcache if you so desire since feature checking is read heavy, as opposed to write heavy.
13
+
14
+ ## Coming Soon™
15
+
16
+ * Web UI (think resque UI for features toggling/status)
17
+ * Optimizations for per request in process caching of trips to the adapter
18
+
19
+ ## Usage
20
+
21
+ The goal of the API for flipper was to have everything revolve around features and what ways they can be enabled. Start with top level and dig into a feature, then dig in further and enable that feature for a given type of access, as opposed to thinking about how the feature will be accessed first (ie: stats.enable vs activate_group(:stats, ...)).
22
+
23
+ ```ruby
24
+ require 'flipper'
25
+ require 'flipper/adapters/memory'
26
+
27
+ # pick an adapter
28
+ adapter = Flipper::Adapters::Memory.new
29
+
30
+ # get a handy dsl instance
31
+ flipper = Flipper.new(adapter)
32
+
33
+ # grab a feature
34
+ search = flipper[:search]
35
+
36
+ # check if that feature is enabled
37
+ if search.enabled?
38
+ puts 'Search away!'
39
+ else
40
+ puts 'No search for you!'
41
+ end
42
+
43
+ puts 'Enabling Search...'
44
+ search.enable
45
+
46
+ # check if that feature is enabled again
47
+ if search.enabled?
48
+ puts 'Search away!'
49
+ else
50
+ puts 'No search for you!'
51
+ end
52
+ ```
53
+
54
+ Of course there are more [examples for you to peruse](https://github.com/jnunemaker/flipper/tree/master/examples).
55
+
56
+ ## Types
57
+
58
+ Out of the box several types of enabling are supported. They are checked in this order.
59
+
60
+ ### 1. Boolean
61
+
62
+ All on or all off. Think top level things like :stats, :search, :logging, etc. Also, an easy way to release a new feature as once a feature is boolean enabled it is on for every situation.
63
+
64
+ ```ruby
65
+ flipper = Flipper.new(adapter)
66
+ flipper[:stats].enable # turn on
67
+ flipper[:stats].disable # turn off
68
+ flipper[:stats].enabled? # check
69
+ ```
70
+
71
+ ### 2. Group
72
+
73
+ Turn on feature based on value of block. Super flexible way to turn on a feature for multiple things (users, people, accounts, etc.)
74
+
75
+ ```ruby
76
+ Flipper.register(:admins) do |actor|
77
+ actor.respond_to?(:admin?) && actor.admin?
78
+ end
79
+
80
+ flipper = Flipper.new(adapter)
81
+ flipper[:stats].enable flipper.group(:admins) # turn on for admins
82
+ flipper[:stats].disable flipper.group(:admins) # turn off for admins
83
+ person = Person.find(params[:id])
84
+ flipper[:stats].enabled? person # check if enabled, returns true if person.admin? is true
85
+ ```
86
+
87
+ There is no requirement that the thing yielded to the block be a user model or whatever. It can be anything you want therefore it is a good idea to check that the thing passed into the group block actually responds to what you are trying.
88
+
89
+ ### 3. Individual Actor
90
+
91
+ Turn on for individual thing. Think enable feature for someone to test or for a buddy.
92
+
93
+ ```ruby
94
+ flipper = Flipper.new(adapter)
95
+
96
+ # convert user or person or whatever to flipper actor for storing and checking
97
+ actor = flipper.actor(user.id)
98
+
99
+ flipper[:stats].enable actor
100
+ flipper[:stats].enabled? actor # true
101
+
102
+ flipper[:stats].disable actor
103
+ flipper[:stats].disabled? actor # true
104
+ ```
105
+
106
+ ### 4. Percentage of Actors
107
+
108
+ Turn this on for a percentage of actors (think users or people). Consistently on or off for this user as long as percentage increases. Think slow rollout of a new feature to users.
109
+
110
+ ```ruby
111
+ flipper = Flipper.new(adapter)
112
+
113
+ # convert user or person or whatever to flipper actor for checking if in percentage
114
+ actor = flipper.actor(user.id)
115
+
116
+ # returns a percentage of actors instance set to 10
117
+ percentage = flipper.actors(10)
118
+
119
+ # turn stats on for 10 percent of users in the system
120
+ flipper[:stats].enable percentage
121
+
122
+ # checks if actor's identifier is in the enabled percentage
123
+ flipper[:stats].enabled? actor
124
+
125
+ ```
126
+
127
+ ### 5. Percentage of Random
128
+
129
+ Turn this on for a random percentage of time. Think load testing new features behind the scenes and such.
130
+
131
+ ```ruby
132
+ flipper = Flipper.new(adapter)
133
+
134
+ # get percentage of random instance set to 5
135
+ percentage = flipper.random(5)
136
+
137
+ # turn on logging for 5 percent of the time randomly
138
+ # could be on during one request and off the next
139
+ # could even be on first time in request and off second time
140
+ flipper[:logging].enable percentage
141
+ ```
142
+
143
+ Randomness is probably not a good idea for enabling new features in the UI. Most of the time you want a feature on or off for a user, but there are definitely times when I have found percentage of random to be very useful.
144
+
145
+
146
+ ## Adapters
147
+
148
+ I plan on supporting in-memory, Mongo, and Redis as adapters for flipper. Others are welcome so please let me know if you create one.
149
+
150
+ ### Memory
151
+
152
+ You can use this for tests if you want. That is pretty much all I use it for.
153
+
154
+ ### Mongo
155
+
156
+ Currently, the mongo adapter stores everything in a single document. This is cool as if you cache that document for the life of a web request or whatever, you can check a ton of features by adding very little burden (1 query per request).
157
+
158
+ ### Redis
159
+
160
+ Redis is great for this type of stuff and it only took a few minutes to implement. The only real problem with redis right now is that automated failover isn't that easy so relying on it for every code path in my app would make me nervous.
161
+
162
+ ## Installation
163
+
164
+ Add this line to your application's Gemfile:
165
+
166
+ gem 'flipper'
167
+
168
+ And then execute:
169
+
170
+ $ bundle
171
+
172
+ Or install it yourself with:
173
+
174
+ $ gem install flipper
175
+
176
+ ## Contributing
177
+
178
+ 1. Fork it
179
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
180
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
181
+ 4. Push to the branch (`git push origin my-new-feature`)
182
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new
6
+
7
+ task :default => :spec
data/examples/basic.rb ADDED
@@ -0,0 +1,27 @@
1
+ require './example_setup'
2
+
3
+ require 'flipper'
4
+ require 'flipper/adapters/memory'
5
+
6
+ # pick an adapter
7
+ adapter = Flipper::Adapters::Memory.new
8
+
9
+ # get a handy dsl instance
10
+ flipper = Flipper.new(adapter)
11
+
12
+ # grab a feature
13
+ search = flipper[:search]
14
+
15
+ perform = lambda do
16
+ # check if that feature is enabled
17
+ if search.enabled?
18
+ puts 'Search away!'
19
+ else
20
+ puts 'No search for you!'
21
+ end
22
+ end
23
+
24
+ perform.call
25
+ puts 'Enabling Search...'
26
+ search.enable
27
+ perform.call
data/examples/dsl.rb ADDED
@@ -0,0 +1,85 @@
1
+ require './example_setup'
2
+
3
+ require 'flipper'
4
+ require 'flipper/adapters/memory'
5
+
6
+ adapter = Flipper::Adapters::Memory.new
7
+ flipper = Flipper.new(adapter)
8
+
9
+ # create a thing with an identifier
10
+ class Person
11
+ attr_reader :id
12
+
13
+ def initialize(id)
14
+ @id = id
15
+ end
16
+ end
17
+
18
+ person = Person.new(1)
19
+
20
+ puts "Stats are disabled by default\n\n"
21
+
22
+ # is a feature enabled
23
+ puts "flipper.enabled? :stats: #{flipper.enabled? :stats}"
24
+
25
+ # is a feature on or off for a particular person
26
+ puts "flipper.enabled? :stats, person: #{flipper.enabled? :stats, person}"
27
+
28
+ # get at a feature
29
+ puts "\nYou can also get an individual feature like this:\nstats = flipper[:stats]\n\n"
30
+ stats = flipper[:stats]
31
+
32
+ # is that feature enabled
33
+ puts "stats.enabled?: #{stats.enabled?}"
34
+
35
+ # is that feature enabled for a particular person
36
+ puts "stats.enabled? person: #{stats.enabled? person}"
37
+
38
+ # enable a feature by name
39
+ puts "\nEnabling stats\n\n"
40
+ flipper.enable :stats
41
+
42
+ # or, you can use the feature to enable
43
+ stats.enable
44
+
45
+ puts "stats.enabled?: #{stats.enabled?}"
46
+ puts "stats.enabled? person: #{stats.enabled? person}"
47
+
48
+ # oh, no, let's turn this baby off
49
+ puts "\nDisabling stats\n\n"
50
+ flipper.disable :stats
51
+
52
+ # or we can disable using feature obviously
53
+ stats.disable
54
+
55
+ puts "stats.enabled?: #{stats.enabled?}"
56
+ puts "stats.disabled?: #{stats.disabled?}"
57
+ puts "stats.enabled? person: #{stats.enabled? person}"
58
+ puts "stats.disabled? person: #{stats.disabled? person}"
59
+ puts
60
+
61
+ # get an instance of the percentage of random type set to 5
62
+ puts flipper.random(5).inspect
63
+
64
+ # get an instance of the percentage of actors type set to 15
65
+ puts flipper.actors(15).inspect
66
+
67
+ # get an instance of an actor using an object that responds to id
68
+ responds_to_id = Struct.new(:id).new(10)
69
+ puts flipper.actor(responds_to_id).inspect
70
+
71
+ # get an instance of an actor using an object that responds to identifier
72
+ responds_to_identifier = Struct.new(:identifier).new(11)
73
+ puts flipper.actor(responds_to_identifier).inspect
74
+
75
+ # get an instance of an actor using a number
76
+ puts flipper.actor(23).inspect
77
+
78
+ # register a top level group
79
+ admins = Flipper.register(:admins) { |actor|
80
+ actor.respond_to?(:admin?) && actor.admin?
81
+ }
82
+ puts admins.inspect
83
+
84
+ # get instance of registered group by name
85
+ puts Flipper.group(:admins).inspect
@@ -0,0 +1,8 @@
1
+ # Nothing to see here... move along.
2
+ # Sets up load path for examples and requires some stuff
3
+ require 'pp'
4
+ require 'pathname'
5
+
6
+ root_path = Pathname(__FILE__).dirname.join('..').expand_path
7
+ lib_path = root_path.join('lib')
8
+ $:.unshift(lib_path)
data/examples/group.rb ADDED
@@ -0,0 +1,36 @@
1
+ require './example_setup'
2
+
3
+ require 'flipper'
4
+ require 'flipper/adapters/memory'
5
+
6
+ adapter = Flipper::Adapters::Memory.new
7
+ flipper = Flipper.new(adapter)
8
+ stats = flipper[:stats]
9
+
10
+ # Register group
11
+ Flipper.register(:admins) do |actor|
12
+ actor.respond_to?(:admin?) && actor.admin?
13
+ end
14
+
15
+ # Some class that represents actor that will be trying to do something
16
+ class User
17
+ def initialize(admin)
18
+ @admin = admin
19
+ end
20
+
21
+ def admin?
22
+ @admin == true
23
+ end
24
+ end
25
+
26
+ admin = User.new(true)
27
+ non_admin = User.new(false)
28
+
29
+ puts "Stats for admin: #{stats.enabled?(admin)}"
30
+ puts "Stats for non_admin: #{stats.enabled?(non_admin)}"
31
+
32
+ puts "\nEnabling Stats for admins...\n\n"
33
+ stats.enable(flipper.group(:admins))
34
+
35
+ puts "Stats for admin: #{stats.enabled?(admin)}"
36
+ puts "Stats for non_admin: #{stats.enabled?(non_admin)}"
@@ -0,0 +1,29 @@
1
+ require './example_setup'
2
+
3
+ require 'flipper'
4
+ require 'flipper/adapters/memory'
5
+
6
+ adapter = Flipper::Adapters::Memory.new
7
+ flipper = Flipper.new(adapter)
8
+ stats = flipper[:stats]
9
+
10
+ # Some class that represents what will be trying to do something
11
+ class User
12
+ attr_reader :id
13
+
14
+ def initialize(id)
15
+ @id = id
16
+ end
17
+ end
18
+
19
+ user1 = User.new(1)
20
+ user2 = User.new(2)
21
+
22
+ puts "Stats for user1: #{stats.enabled?(flipper.actor(user1))}"
23
+ puts "Stats for user2: #{stats.enabled?(flipper.actor(user2))}"
24
+
25
+ puts "\nEnabling stats for user1...\n\n"
26
+ stats.enable(flipper.actor(user1))
27
+
28
+ puts "Stats for user1: #{stats.enabled?(flipper.actor(user1))}"
29
+ puts "Stats for user2: #{stats.enabled?(flipper.actor(user2))}"