flipflop 2.0.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.
- checksums.yaml +15 -0
- data/.gitignore +5 -0
- data/.travis.yml +51 -0
- data/Gemfile +20 -0
- data/LICENSE +22 -0
- data/README.md +261 -0
- data/Rakefile +16 -0
- data/app/assets/stylesheets/flipflop.scss +109 -0
- data/app/controllers/concerns/flipflop/environment_filters.rb +5 -0
- data/app/controllers/flipflop/features_controller.rb +59 -0
- data/app/controllers/flipflop/strategies_controller.rb +30 -0
- data/app/models/flipflop/feature.rb +3 -0
- data/app/views/flipflop/features/index.html.erb +60 -0
- data/app/views/layouts/flipflop.html.erb +1 -0
- data/config/routes.rb +5 -0
- data/flipflop.gemspec +23 -0
- data/lib/flipflop/configurable.rb +27 -0
- data/lib/flipflop/engine.rb +58 -0
- data/lib/flipflop/facade.rb +23 -0
- data/lib/flipflop/feature_cache.rb +64 -0
- data/lib/flipflop/feature_definition.rb +15 -0
- data/lib/flipflop/feature_set.rb +99 -0
- data/lib/flipflop/strategies/abstract_strategy.rb +103 -0
- data/lib/flipflop/strategies/active_record_strategy.rb +43 -0
- data/lib/flipflop/strategies/cookie_strategy.rb +44 -0
- data/lib/flipflop/strategies/default_strategy.rb +15 -0
- data/lib/flipflop/strategies/lambda_strategy.rb +25 -0
- data/lib/flipflop/strategies/query_string_strategy.rb +17 -0
- data/lib/flipflop/strategies/session_strategy.rb +29 -0
- data/lib/flipflop/strategies/test_strategy.rb +40 -0
- data/lib/flipflop/version.rb +3 -0
- data/lib/flipflop.rb +26 -0
- data/lib/generators/flipflop/features/USAGE +8 -0
- data/lib/generators/flipflop/features/features_generator.rb +7 -0
- data/lib/generators/flipflop/features/templates/features.rb +21 -0
- data/lib/generators/flipflop/install/install_generator.rb +21 -0
- data/lib/generators/flipflop/migration/USAGE +5 -0
- data/lib/generators/flipflop/migration/migration_generator.rb +23 -0
- data/lib/generators/flipflop/migration/templates/create_features.rb +10 -0
- data/lib/generators/flipflop/routes/USAGE +7 -0
- data/lib/generators/flipflop/routes/routes_generator.rb +5 -0
- data/test/integration/app_test.rb +32 -0
- data/test/integration/dashboard_test.rb +162 -0
- data/test/test_helper.rb +96 -0
- data/test/unit/configurable_test.rb +104 -0
- data/test/unit/feature_cache_test.rb +142 -0
- data/test/unit/feature_definition_test.rb +42 -0
- data/test/unit/feature_set_test.rb +136 -0
- data/test/unit/flipflop_test.rb +99 -0
- data/test/unit/strategies/abstract_strategy_request_test.rb +42 -0
- data/test/unit/strategies/abstract_strategy_test.rb +124 -0
- data/test/unit/strategies/active_record_strategy_test.rb +157 -0
- data/test/unit/strategies/cookie_strategy_test.rb +126 -0
- data/test/unit/strategies/default_strategy_test.rb +44 -0
- data/test/unit/strategies/lambda_strategy_test.rb +137 -0
- data/test/unit/strategies/query_string_strategy_test.rb +70 -0
- data/test/unit/strategies/session_strategy_test.rb +101 -0
- data/test/unit/strategies/test_strategy_test.rb +76 -0
- metadata +134 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
Mjk5MWNkOGUyOGViMGIxOTJmZmY0NTc5NjU3Yjk3NjM0NWIyNDE1ZA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NjRiOGU3YTg3ZDAyYmMxM2RmZmEzZDcyOTBlZTNjZmRlMWI4YTdiNA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YTRkOTdlMjAyMzVjYjdjOWJlMDkxOWYxZmYwMmUzMDMxYmE3YjcyNmI5ZWZk
|
10
|
+
MWMxYjY1ZTU2MTBiOWYxMDNlOGI0MDE2Y2RmYWQ3ZTZiMTUzNjE5ZDA2NmEw
|
11
|
+
YTViYjdmNzNkNTljZWFiMTUwOWZmNDhhMDgzYThiNGJmMTdlM2I=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MTllZjMwYjA5YWZmMzVjNGM3YjRmMGE0Mjk0NTg3MGY4NmY2MTgwNjcxOWVk
|
14
|
+
NzE3NzllOWQwZDgzN2FkYjg2YThiMThlMDI4ZGMzODk5ZjFiYTIzYzQ3MGU5
|
15
|
+
NDU0OTcxYjdjYzJkZWIwNTI5OTdkOWIxMGRjYmJkZDVmYmM4OTI=
|
data/.travis.yml
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 2.0
|
4
|
+
- 2.1
|
5
|
+
- 2.2
|
6
|
+
- 2.3.0
|
7
|
+
- jruby-9.0.5.0
|
8
|
+
- rbx-head
|
9
|
+
- ruby-head
|
10
|
+
- jruby-head
|
11
|
+
env:
|
12
|
+
- RAILS_VERSION=4.0
|
13
|
+
- RAILS_VERSION=4.1
|
14
|
+
- RAILS_VERSION=4.2
|
15
|
+
- RAILS_VERSION=5.0.0.beta3
|
16
|
+
- RAILS_VERSION=master
|
17
|
+
matrix:
|
18
|
+
exclude:
|
19
|
+
- rvm: 2.0
|
20
|
+
env: RAILS_VERSION=5.0.0.beta3
|
21
|
+
- rvm: 2.0
|
22
|
+
env: RAILS_VERSION=master
|
23
|
+
- rvm: 2.1
|
24
|
+
env: RAILS_VERSION=5.0.0.beta3
|
25
|
+
- rvm: 2.1
|
26
|
+
env: RAILS_VERSION=master
|
27
|
+
- rvm: 2.2
|
28
|
+
env: RAILS_VERSION=5.0.0.beta3
|
29
|
+
- rvm: 2.2
|
30
|
+
env: RAILS_VERSION=master
|
31
|
+
allow_failures:
|
32
|
+
- rvm: jruby-9.0.5.0
|
33
|
+
env: RAILS_VERSION=5.0.0.beta3
|
34
|
+
- rvm: jruby-9.0.5.0
|
35
|
+
env: RAILS_VERSION=master
|
36
|
+
- rvm: rbx-head
|
37
|
+
- rvm: ruby-head
|
38
|
+
- rvm: jruby-head
|
39
|
+
notifications:
|
40
|
+
email: false
|
41
|
+
slack:
|
42
|
+
secure: crOO1QGnFn9T1DpVgxkukTSiN8lQq09X8WF8oi1Eoa7Liex4gzWq7f8wlIXPrRFAsNjU47fSrz+L1C6Eg068Vd2df9pkqjW0pLNeqUViJ35TzpYISTtzJrX+x3nvCQLlvS4leP6lkijlsvlu1IN0xnXadW5cmcMoEcPo4Yma1RUklwlreSRIEmJiutYKVFRw3gIZA9vsnXNEcD408mvSY/8Kuw+hmRQupODUalXDpZo1q3HH+ZPQq+/rGuJ7XRf9sBtxjpUF0G4FJZQhVP4CrLNYVBE/83rHJ6HSf6u3SlYVIMiautq0nWpVLPHUrkOPJVeVh6EPtoFeI/cehH1NyoAVvL5a39wFRBlJ4jVPWUrrnihJT/6+P6GM9PSnYogxtIoTsdrYES2FgtWGgwG5uLyw8U6bW7G7rCzQwBP7enVHWVCbDgdSSjE1Mg1I9qhRuL6pHs5des4VKk6pfD3p+BRqLmOZR2jx4v8MFwakSFqQWOMxaD0U1lfxecqSx9OkwWEhCFSnHeXeHInEhY6qKCdZZzT+beYn0xppUMPJGMTqe5+po8gL+5MxQwI8Xs/5hSve5frfmuS7UQf5BnFMOzwoThQrXCFRz58wXvcZTD9eTdVBV44Hsi5OLdYn9K58sNUhgcxGfRgdE7Gy9P7DD4fHPGakD/Tz++HuCmCUXNs=
|
43
|
+
deploy:
|
44
|
+
provider: rubygems
|
45
|
+
api_key:
|
46
|
+
secure: ObuFc5QWnSgraKzYXLT5EhnlGm/+BQ2IN46q3ykXdk8m80ajpWD0/rqtCmu5SiBd6Z56CVZe2zSsEpGhN7/pC5kdG0hYzdpvbgX9IxJNMjYb7rNh2onXdIHVd3yk3qdNgI8hmrgtocJPdtPXqbZSY6KOSH6eb5rPDmA/bWwyREdicW5KN8ZWsXiLzKXoFZ6xvkNFbAjY8AicIfG+dHBFbx+q1xBOvbS44M+VIt/Qsknt9m5CYaWJ0AK/RZoyRhs0F0yYQSLu0mVt7JbHOL05SqQ3uCNlwiEqX+fIiN2mPb24Xxy6SVXr1kD0+H4EZHpgDeUlylO2myYJBFBnjzKXXr0qh3YTxaEKlWO7HnUdnJDRQ+bjyT0USBv7gXO2fEIBN864AGZf+3cL0aKRW3n3cyqrhiUxim7gro7eQSh6t6x5o+LGYw6dDdhR53XIFZoh0s/azYBbgl0bfM3juBMrD3e4w/ieaE9iI+NVj4Z4DYDRtbGIqxsuLwRmSkg3epvBc4Pa7vTDuCyVtd9lpFK55V1TysxhcPu0lqHf+SNnW3+DDwp+CQusQhFIsOcIQdr6PBNSTP5ZtEEqyEMfmt2QK2BEOYUuL6TxIUa9xuu8zMUahSaLOvekM5R+EwW04C3T5hWbPs7W1Ks2jcVzkL2iNd3I18pW9VvkXNAqveuoT28=
|
47
|
+
gem: flipflop
|
48
|
+
on:
|
49
|
+
tags: true
|
50
|
+
repo: voormedia/flipflop
|
51
|
+
ruby: 2.3.0
|
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
gemspec
|
3
|
+
|
4
|
+
group :test do
|
5
|
+
version = ENV["RAILS_VERSION"] || "master"
|
6
|
+
if version == "master"
|
7
|
+
gem "rails", github: "rails/rails"
|
8
|
+
else
|
9
|
+
gem "rails", "~> #{version}.0"
|
10
|
+
end
|
11
|
+
|
12
|
+
gem "sqlite3", ">= 1.3", platform: :ruby
|
13
|
+
gem "activerecord-jdbcsqlite3-adapter", platform: :jruby
|
14
|
+
gem "minitest", ">= 4.2"
|
15
|
+
gem "capybara", ">= 2.6"
|
16
|
+
|
17
|
+
if ENV["RAILS_VERSION"] == "4.0"
|
18
|
+
gem "minitest-rails", platform: :jruby
|
19
|
+
end
|
20
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2011-2013 Learnable Pty Ltd
|
4
|
+
Copyright (c) 2016 Voormedia
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
8
|
+
in the Software without restriction, including without limitation the rights
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
11
|
+
furnished to do so, subject to the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
14
|
+
all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,261 @@
|
|
1
|
+
[<img src="https://travis-ci.org/voormedia/flipflop.svg?branch=master" alt="Build Status">](https://travis-ci.org/voormedia/flipflop)
|
2
|
+
|
3
|
+
# Flipflop your features
|
4
|
+
|
5
|
+
**Flipflop** provides a declarative, layered way of enabling and disabling
|
6
|
+
application functionality at run-time. It is originally based on
|
7
|
+
[Flip](https://github.com/pda/flip). Compared to the original gem **Flipflop** has:
|
8
|
+
* an improved dashboard
|
9
|
+
* thread safety
|
10
|
+
* better database performance due to per-request caching, enabled by default
|
11
|
+
* more strategies (query strings, sessions, custom code)
|
12
|
+
* more strategy options (cookie options, strategy names and descriptions, custom database models)
|
13
|
+
* the ability to use the same strategy twice, with different options
|
14
|
+
* configuration in a fixed location (`config/features.rb`) that is usable even if you don't use the database strategy
|
15
|
+
* dashboard is inaccessible in production by default, for safety in case of misconfiguration
|
16
|
+
* removes controller filters and view helpers, to promote uniform semantics to check for features (facilitates project-wide searching)
|
17
|
+
|
18
|
+
You can configure strategy layers that will evaluate if a feature is currently
|
19
|
+
enabled or disabled. Available strategies are:
|
20
|
+
* a per-feature default setting
|
21
|
+
* database (with Active Record), to flipflop features site-wide for all users
|
22
|
+
* cookie or session, to flipflop features for single users
|
23
|
+
* query string parameters, to flipflop features occasionally (in development mode for example)
|
24
|
+
* custom strategy code
|
25
|
+
|
26
|
+
Flipflop has a dashboard interface that's easy to understand and use.
|
27
|
+
|
28
|
+
[<img src="https://raw.githubusercontent.com/voormedia/flipflop/screenshots/dashboard.png" alt="Dashboard">](https://raw.githubusercontent.com/voormedia/flipflop/screenshots/dashboard.png)
|
29
|
+
|
30
|
+
## Installation
|
31
|
+
|
32
|
+
Add the gem to your `Gemfile`:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
gem "flipflop"
|
36
|
+
```
|
37
|
+
|
38
|
+
Generate routes, feature settings and database migration:
|
39
|
+
|
40
|
+
```
|
41
|
+
rails g flipflop:install
|
42
|
+
```
|
43
|
+
|
44
|
+
Run the migration to store feature settings in your database:
|
45
|
+
|
46
|
+
```
|
47
|
+
rake db:migrate
|
48
|
+
```
|
49
|
+
|
50
|
+
## Declaring features
|
51
|
+
|
52
|
+
Features and strategies are declared in `config/features.rb`:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
Flipflop.configure do
|
56
|
+
# Strategies will be used in the order listed here.
|
57
|
+
strategy :cookie
|
58
|
+
strategy :active_record
|
59
|
+
strategy :default
|
60
|
+
|
61
|
+
# Basic feature declaration:
|
62
|
+
feature :shiny_things
|
63
|
+
|
64
|
+
# Enable features by default:
|
65
|
+
feature :world_domination, default: true
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
This file is automatically reloaded in development mode. No need to restart
|
70
|
+
your server after making changes.
|
71
|
+
|
72
|
+
## Strategies
|
73
|
+
|
74
|
+
The following strategies are provided:
|
75
|
+
* `:active_record` – Save feature settings in the database.
|
76
|
+
* `:class` – Provide the feature model. `Flipflop::Feature` by default (which uses the table `features`). Honors `default_scope` when features are resolved or switched on/off.
|
77
|
+
* `:cookie` – Save feature settings in browser cookies for the current user.
|
78
|
+
* `:path` – The path for which the cookies apply. Defaults to the root of the application.
|
79
|
+
* `:domain` – Cookie domain. Is `nil` by default (no specific domain). Can be `:all` to use the topmost domain. Can be an array of domains.
|
80
|
+
* `:secure` – Only set cookies if the connection is secured with TLS. Default is `false`.
|
81
|
+
* `:httponly` – Whether the cookies are accessible via scripting or only HTTP. Default is `false`.
|
82
|
+
* `:query_string` – Interpret query string parameters as features. This strategy is only used for resolving. It does not allow switching features on/off.
|
83
|
+
* `:session` – Save feature settings in the current user's application session.
|
84
|
+
* `:default` – Not strictly needed, all feature defaults will be applied if no strategies match a feature. Include this strategy to determine the order of using the default value, and to make it appear in the dashboard.
|
85
|
+
|
86
|
+
All strategies support these options, to change the appearance of the dashboard:
|
87
|
+
* `:name` – The name of the strategy. Defaults to the name of the selected strategy.
|
88
|
+
* `:description` – The description of the strategy. Every strategy has a default description.
|
89
|
+
* `:hidden` – Optionally hides the strategy from the dashboard. Default is `false`.
|
90
|
+
|
91
|
+
## Checking if a feature is enabled
|
92
|
+
|
93
|
+
`Flipflop.enabled?` or the dynamic predicate methods can be used to check
|
94
|
+
feature state:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
Flipflop.enabled?(:world_domination) # true
|
98
|
+
Flipflop.world_domination? # true
|
99
|
+
|
100
|
+
Flipflop.enabled?(:shiny_things) # false
|
101
|
+
Flipflop.shiny_things? # false
|
102
|
+
```
|
103
|
+
|
104
|
+
This works everywhere. In your views:
|
105
|
+
|
106
|
+
```erb
|
107
|
+
<div>
|
108
|
+
<% if Flipflop.world_domination? %>
|
109
|
+
<%= link_to "Dominate World", world_dominations_path %>
|
110
|
+
<% end %>
|
111
|
+
</div>
|
112
|
+
```
|
113
|
+
|
114
|
+
In your controllers:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
class ShinyThingsController < ApplicationController
|
118
|
+
def index
|
119
|
+
return head :forbidden unless Flipflop.shiny_things?
|
120
|
+
# Proceed with shiny things...
|
121
|
+
end
|
122
|
+
end
|
123
|
+
```
|
124
|
+
|
125
|
+
In your models:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
class ShinyThing < ActiveRecord::Base
|
129
|
+
after_initialize do
|
130
|
+
if !Flipflop.shiny_things?
|
131
|
+
raise ActiveRecord::RecordNotFound
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
```
|
136
|
+
|
137
|
+
## Custom strategies
|
138
|
+
|
139
|
+
Custom light-weight strategies can be defined with a block:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
Flipflop.configure do
|
143
|
+
strategy :random do |feature|
|
144
|
+
rand(2).zero?
|
145
|
+
end
|
146
|
+
# ...
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
You can define your own custom strategies by inheriting from `Flipflop::Strategies::AbstractStrategy`:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
class UserPreferenceStrategy < Flipflop::Strategies::AbstractStrategy
|
154
|
+
class << self
|
155
|
+
def default_description
|
156
|
+
"Allows configuration of features per user."
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def switchable?
|
161
|
+
# Can only switch features on/off if we have the user's session.
|
162
|
+
# The `request` method is provided by AbstractStrategy.
|
163
|
+
request?
|
164
|
+
end
|
165
|
+
|
166
|
+
def enabled?(feature)
|
167
|
+
# Can only check features if we have the user's session.
|
168
|
+
return unless request?
|
169
|
+
find_current_user.enabled_features[feature]
|
170
|
+
end
|
171
|
+
|
172
|
+
def switch!(feature, enabled)
|
173
|
+
user = find_current_user
|
174
|
+
user.enabled_features[feature] = enabled
|
175
|
+
user.save!
|
176
|
+
end
|
177
|
+
|
178
|
+
def clear!(feature)
|
179
|
+
user = find_current_user
|
180
|
+
user.enabled_features.delete(feature)
|
181
|
+
user.save!
|
182
|
+
end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
def find_current_user
|
187
|
+
# The `request` method is provided by AbstractStrategy.
|
188
|
+
User.find_by_id(request.session[:user_id])
|
189
|
+
end
|
190
|
+
end
|
191
|
+
```
|
192
|
+
|
193
|
+
Use it in `config/features.rb`:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
Flipflop.configure do
|
197
|
+
strategy UserPreferenceStrategy # name: "my strategy", description: "..."
|
198
|
+
end
|
199
|
+
```
|
200
|
+
|
201
|
+
## Dashboard access control
|
202
|
+
|
203
|
+
The dashboard provides visibility and control over the features.
|
204
|
+
|
205
|
+
You don't want the dashboard to be public. For that reason it is only available
|
206
|
+
in the development and test environments by default. Here's one way of
|
207
|
+
implementing access control.
|
208
|
+
|
209
|
+
In `app/config/application.rb`:
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
config.flipflop.dashboard_access_filter = :require_authenticated_user
|
213
|
+
```
|
214
|
+
|
215
|
+
In `app/controllers/application_controller.rb`:
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
class ApplicationController < ActionController::Base
|
219
|
+
def require_authenticated_user
|
220
|
+
head :forbidden unless User.logged_in?
|
221
|
+
end
|
222
|
+
end
|
223
|
+
```
|
224
|
+
|
225
|
+
Or directly in `app/config/application.rb`:
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
config.flipflop.dashboard_access_filter = -> {
|
229
|
+
head :forbidden unless User.logged_in?
|
230
|
+
}
|
231
|
+
```
|
232
|
+
|
233
|
+
## Testing
|
234
|
+
|
235
|
+
In your test environment, you typically want to keep your features. But to make
|
236
|
+
testing easier, you may not want to use any of the strategies you use in
|
237
|
+
development and production. You can replace all strategies with a single
|
238
|
+
`:test` strategy by calling `Flipflop::FeatureSet.current.test!`. The test
|
239
|
+
strategy will be returned. You can use this strategy to enable and disable
|
240
|
+
features.
|
241
|
+
|
242
|
+
```ruby
|
243
|
+
describe WorldDomination do
|
244
|
+
before do
|
245
|
+
test_strategy = Flipflop::FeatureSet.current.test!
|
246
|
+
test_strategy.switch!(:world_domination, true)
|
247
|
+
end
|
248
|
+
|
249
|
+
it "should dominate the world" do
|
250
|
+
# ...
|
251
|
+
end
|
252
|
+
end
|
253
|
+
```
|
254
|
+
|
255
|
+
If you are not happy with the default test strategy (which is essentially a
|
256
|
+
simple thread-safe hash object), you can provide your own implementation as
|
257
|
+
argument to the `test!` method.
|
258
|
+
|
259
|
+
## License
|
260
|
+
|
261
|
+
This software is licensed under the MIT License. [View the license](LICENSE).
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
|
+
|
4
|
+
Rake::TestTask.new("test") do |test|
|
5
|
+
test.pattern = "test/**/*_test.rb"
|
6
|
+
end
|
7
|
+
|
8
|
+
Rake::TestTask.new("test:unit") do |test|
|
9
|
+
test.pattern = "test/unit/**/*_test.rb"
|
10
|
+
end
|
11
|
+
|
12
|
+
Rake::TestTask.new("test:integration") do |test|
|
13
|
+
test.pattern = "test/integration/**/*_test.rb"
|
14
|
+
end
|
15
|
+
|
16
|
+
task default: :test
|
@@ -0,0 +1,109 @@
|
|
1
|
+
$enable-transitions: true;
|
2
|
+
@import "bootstrap";
|
3
|
+
|
4
|
+
section.flipflop {
|
5
|
+
@extend .container-fluid;
|
6
|
+
margin: 5rem 0 0;
|
7
|
+
|
8
|
+
table {
|
9
|
+
@extend .table;
|
10
|
+
@extend .table-striped;
|
11
|
+
|
12
|
+
thead {
|
13
|
+
@extend .thead-inverse;
|
14
|
+
|
15
|
+
th {
|
16
|
+
position: relative;
|
17
|
+
cursor: default;
|
18
|
+
|
19
|
+
&[data-tooltip]:before, &[data-tooltip]:after {
|
20
|
+
@include transition(all 0.2s ease-out);
|
21
|
+
transform: translateY(0.2rem) translateZ(0);
|
22
|
+
opacity: 0;
|
23
|
+
|
24
|
+
display: block;
|
25
|
+
position: absolute;
|
26
|
+
}
|
27
|
+
|
28
|
+
&[data-tooltip]:before {
|
29
|
+
content: attr(data-tooltip);
|
30
|
+
width: 98%;
|
31
|
+
left: 0;
|
32
|
+
bottom: 3.75rem;
|
33
|
+
margin: 0;
|
34
|
+
padding: 0.5rem 0.75rem;
|
35
|
+
background: $gray;
|
36
|
+
border-radius: 0.2rem;
|
37
|
+
font-size: 0.875rem;
|
38
|
+
font-weight: normal;
|
39
|
+
pointer-events: none;
|
40
|
+
}
|
41
|
+
|
42
|
+
&[data-tooltip]:after {
|
43
|
+
content: " ";
|
44
|
+
width: 0;
|
45
|
+
height: 0;
|
46
|
+
left: 1rem;
|
47
|
+
bottom: 3.25rem;
|
48
|
+
border-left: solid transparent 0.5rem;
|
49
|
+
border-right: solid transparent 0.5rem;
|
50
|
+
border-top: solid $gray 0.5rem;
|
51
|
+
}
|
52
|
+
|
53
|
+
&:hover {
|
54
|
+
&[data-tooltip]:before, &[data-tooltip]:after {
|
55
|
+
transform: translateY(0) translateZ(0);
|
56
|
+
opacity: 1;
|
57
|
+
}
|
58
|
+
}
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
tbody {
|
63
|
+
td.status {
|
64
|
+
width: 2rem;
|
65
|
+
font-size: 1.1rem;
|
66
|
+
|
67
|
+
span {
|
68
|
+
width: 2rem;
|
69
|
+
@extend .label;
|
70
|
+
@extend .label-pill;
|
71
|
+
&.on { @extend .label-success; }
|
72
|
+
&.off { @extend .label-default; }
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
td.name {
|
77
|
+
min-width: 11rem;
|
78
|
+
padding-top: 0.9rem;
|
79
|
+
font-weight: bold;
|
80
|
+
}
|
81
|
+
|
82
|
+
td.description {
|
83
|
+
min-width: 11rem;
|
84
|
+
padding-top: 0.9rem;
|
85
|
+
}
|
86
|
+
|
87
|
+
td.toggle {
|
88
|
+
min-width: 11rem;
|
89
|
+
|
90
|
+
div.toolbar {
|
91
|
+
@extend .btn-toolbar;
|
92
|
+
margin-left: 0;
|
93
|
+
|
94
|
+
div.group {
|
95
|
+
@extend .btn-group;
|
96
|
+
@extend .btn-group-sm;
|
97
|
+
|
98
|
+
input[type=submit] {
|
99
|
+
@extend .btn;
|
100
|
+
@extend .btn-sm;
|
101
|
+
&.active { @extend .btn-primary; }
|
102
|
+
&:not(.active) { @extend .btn-secondary; }
|
103
|
+
}
|
104
|
+
}
|
105
|
+
}
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|
109
|
+
}
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "bootstrap"
|
2
|
+
|
3
|
+
module Flipflop
|
4
|
+
class FeaturesController < ApplicationController
|
5
|
+
include EnvironmentFilters
|
6
|
+
|
7
|
+
layout "flipflop"
|
8
|
+
|
9
|
+
def index
|
10
|
+
@feature_set = FeaturesPresenter.new(FeatureSet.current)
|
11
|
+
end
|
12
|
+
|
13
|
+
class FeaturesPresenter
|
14
|
+
include Flipflop::Engine.routes.url_helpers
|
15
|
+
|
16
|
+
def initialize(feature_set)
|
17
|
+
@cache = {}
|
18
|
+
@feature_set = feature_set
|
19
|
+
end
|
20
|
+
|
21
|
+
def strategies
|
22
|
+
@feature_set.strategies.reject(&:hidden?)
|
23
|
+
end
|
24
|
+
|
25
|
+
def features
|
26
|
+
@feature_set.features
|
27
|
+
end
|
28
|
+
|
29
|
+
def status(feature)
|
30
|
+
cache(nil, feature) do
|
31
|
+
status_to_s(@feature_set.enabled?(feature.key))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def strategy_status(strategy, feature)
|
36
|
+
cache(strategy, feature) do
|
37
|
+
status_to_s(strategy.enabled?(feature.key))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def switch_url(strategy, feature)
|
42
|
+
feature_strategy_path(feature.key, strategy.key)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def cache(strategy, feature)
|
48
|
+
key = feature.key.to_s + (strategy ? "-" + strategy.key.to_s : "")
|
49
|
+
return @cache[key] if @cache.has_key?(key)
|
50
|
+
@cache[key] = yield
|
51
|
+
end
|
52
|
+
|
53
|
+
def status_to_s(status)
|
54
|
+
return "on" if status == true
|
55
|
+
return "off" if status == false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Flipflop
|
2
|
+
class StrategiesController < ApplicationController
|
3
|
+
include EnvironmentFilters
|
4
|
+
include Engine.routes.url_helpers
|
5
|
+
|
6
|
+
def update
|
7
|
+
strategy.switch!(feature_key, enable?)
|
8
|
+
redirect_to(flipflop.features_url)
|
9
|
+
end
|
10
|
+
|
11
|
+
def destroy
|
12
|
+
strategy.clear!(feature_key)
|
13
|
+
redirect_to(flipflop.features_url)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def enable?
|
19
|
+
params[:commit].to_s.downcase.include?("on")
|
20
|
+
end
|
21
|
+
|
22
|
+
def feature_key
|
23
|
+
params[:feature_id].to_sym
|
24
|
+
end
|
25
|
+
|
26
|
+
def strategy
|
27
|
+
FeatureSet.current.strategy(params[:id])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
<section class="flipflop">
|
2
|
+
<h1>Feature Flipflop</h1>
|
3
|
+
<table>
|
4
|
+
<thead>
|
5
|
+
<tr>
|
6
|
+
<th></th>
|
7
|
+
<th class="name">Feature</th>
|
8
|
+
<th class="description">Description</th>
|
9
|
+
<% @feature_set.strategies.each do |strategy| -%>
|
10
|
+
<th data-tooltip="<%= strategy.description -%>">
|
11
|
+
<%= strategy.name.humanize -%>
|
12
|
+
</th>
|
13
|
+
<% end -%>
|
14
|
+
</tr>
|
15
|
+
</thead>
|
16
|
+
<tbody>
|
17
|
+
<% @feature_set.features.each do |feature| -%>
|
18
|
+
<tr data-feature="<%= feature.name.dasherize.parameterize %>">
|
19
|
+
<td class="status">
|
20
|
+
<span class="<%= @feature_set.status(feature) -%>"><%= @feature_set.status(feature) -%></span>
|
21
|
+
</td>
|
22
|
+
<td class="name"><%= feature.name.humanize -%></td>
|
23
|
+
<td class="description"><%= feature.description -%></td>
|
24
|
+
|
25
|
+
<% @feature_set.strategies.each do |strategy| -%>
|
26
|
+
<td class="toggle" data-strategy="<%= strategy.name.dasherize.parameterize %>">
|
27
|
+
<div class="toolbar">
|
28
|
+
<%= form_tag(@feature_set.switch_url(strategy, feature), method: :put) do -%>
|
29
|
+
<div class="group">
|
30
|
+
<%= submit_tag "on",
|
31
|
+
type: "submit",
|
32
|
+
class: @feature_set.strategy_status(strategy, feature) == "on" ? "active" : nil,
|
33
|
+
disabled: strategy.switchable? ? false : true
|
34
|
+
-%>
|
35
|
+
|
36
|
+
<%= submit_tag "off",
|
37
|
+
type: "submit",
|
38
|
+
class: @feature_set.strategy_status(strategy, feature) == "off" ? "active" : nil,
|
39
|
+
disabled: strategy.switchable? ? false : true
|
40
|
+
-%>
|
41
|
+
</div>
|
42
|
+
<% end -%>
|
43
|
+
|
44
|
+
<% if strategy.switchable? -%>
|
45
|
+
<div class="group">
|
46
|
+
<% unless @feature_set.strategy_status(strategy, feature).blank? -%>
|
47
|
+
<%= form_tag(@feature_set.switch_url(strategy, feature), method: :delete) do -%>
|
48
|
+
<%= submit_tag "clear", type: "submit" -%>
|
49
|
+
<% end -%>
|
50
|
+
<% end -%>
|
51
|
+
</div>
|
52
|
+
<% end -%>
|
53
|
+
</div>
|
54
|
+
</td>
|
55
|
+
<% end -%>
|
56
|
+
</tr>
|
57
|
+
<% end -%>
|
58
|
+
</tbody>
|
59
|
+
</table>
|
60
|
+
</div>
|
@@ -0,0 +1 @@
|
|
1
|
+
<!doctype html><head><title></title><%= stylesheet_link_tag :flipflop -%></head><body><%= yield %></body>
|