rack-flags 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/rack-flags/admin_app.rb +120 -0
- data/lib/rack-flags/config.rb +41 -0
- data/lib/rack-flags/cookie_codec.rb +66 -0
- data/lib/rack-flags/rack_middleware.rb +19 -0
- data/lib/rack-flags/reader.rb +61 -0
- data/lib/rack-flags/version.rb +3 -0
- data/lib/rack-flags.rb +15 -0
- data/resources/admin_app/index.html.erb +35 -0
- data/resources/admin_app/style.css +35 -0
- data/spec/acceptance/administering_feature_flags_spec.rb +93 -0
- data/spec/acceptance/reading_feature_flags_spec.rb +73 -0
- data/spec/acceptance/spec_helper.rb +39 -0
- data/spec/acceptance/support/reader_app.rb +24 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/unit/admin_app_spec.rb +143 -0
- data/spec/unit/config_spec.rb +65 -0
- data/spec/unit/cookie_codec_spec.rb +63 -0
- data/spec/unit/middleware_spec.rb +87 -0
- data/spec/unit/rack_flags_module_methods_spec.rb +18 -0
- data/spec/unit/reader_spec.rb +100 -0
- data/spec/unit/spec_helper.rb +1 -0
- metadata +214 -0
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module RackFlags
|
4
|
+
class FullFlagPresenter
|
5
|
+
|
6
|
+
def initialize(full_flag)
|
7
|
+
@full_flag = full_flag
|
8
|
+
end
|
9
|
+
|
10
|
+
def default
|
11
|
+
@full_flag.default ? 'On' : 'Off'
|
12
|
+
end
|
13
|
+
|
14
|
+
def name
|
15
|
+
@full_flag.name
|
16
|
+
end
|
17
|
+
|
18
|
+
def description
|
19
|
+
@full_flag.description
|
20
|
+
end
|
21
|
+
|
22
|
+
def checked_attribute_for(state)
|
23
|
+
state == selected_state ? 'checked' : ''
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def selected_state
|
29
|
+
case @full_flag.override
|
30
|
+
when nil then :default
|
31
|
+
when true then :on
|
32
|
+
else :off
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
class AdminApp
|
39
|
+
def call(env)
|
40
|
+
request = Rack::Request.new(env)
|
41
|
+
|
42
|
+
if request.path_info.chomp("/").empty?
|
43
|
+
handle_root(request)
|
44
|
+
else
|
45
|
+
handle_non_root(request)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def handle_root(request)
|
52
|
+
case
|
53
|
+
when request.get? then handle_root_get(request)
|
54
|
+
when request.post? then handle_root_post(request)
|
55
|
+
else
|
56
|
+
not_allowed
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def handle_root_get(request)
|
61
|
+
reader = RackFlags.for_env(request.env)
|
62
|
+
|
63
|
+
template = ERB.new(resource('index.html.erb'))
|
64
|
+
|
65
|
+
|
66
|
+
flag_presenters = reader.full_flags.map{ |flag| FullFlagPresenter.new(flag) }
|
67
|
+
view_model = OpenStruct.new(
|
68
|
+
:css_href => "#{request.path}/style.css",
|
69
|
+
:flags => flag_presenters
|
70
|
+
)
|
71
|
+
|
72
|
+
[
|
73
|
+
200,
|
74
|
+
{'Content-Type'=>'text/html'},
|
75
|
+
[template.result( view_model.instance_eval{ binding } )]
|
76
|
+
]
|
77
|
+
end
|
78
|
+
|
79
|
+
def handle_root_post(request)
|
80
|
+
overrides = request.POST.inject({}) do |overrides, (flag_name, form_param_flag_state)|
|
81
|
+
overrides[flag_name.downcase.to_sym] = flag_value_for(form_param_flag_state)
|
82
|
+
overrides
|
83
|
+
end
|
84
|
+
|
85
|
+
cookie = CookieCodec.new.generate_cookie_from(overrides)
|
86
|
+
|
87
|
+
response = Rack::Response.new
|
88
|
+
response.redirect(request.script_name, 303)
|
89
|
+
response.set_cookie(CookieCodec::COOKIE_KEY, cookie)
|
90
|
+
|
91
|
+
response.finish
|
92
|
+
end
|
93
|
+
|
94
|
+
def handle_non_root(request)
|
95
|
+
Rack::File.new( RackFlags.path_for_resource('admin_app') ).call(request.env)
|
96
|
+
end
|
97
|
+
|
98
|
+
def not_allowed
|
99
|
+
[405, {}, ['405 - METHOD NOT ALLOWED']]
|
100
|
+
end
|
101
|
+
|
102
|
+
def flag_value_for(form_param_flag_state)
|
103
|
+
flag_states = {
|
104
|
+
on: true,
|
105
|
+
off: false,
|
106
|
+
default: nil
|
107
|
+
}
|
108
|
+
flag_states[form_param_flag_state.to_sym]
|
109
|
+
end
|
110
|
+
|
111
|
+
def resource_root
|
112
|
+
RackFlags.path_for_resource('admin_app')
|
113
|
+
end
|
114
|
+
|
115
|
+
def resource(filename)
|
116
|
+
File.read(RackFlags.path_for_resource(File.join('admin_app',filename)))
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module RackFlags
|
4
|
+
|
5
|
+
class Config
|
6
|
+
attr_reader :flags
|
7
|
+
|
8
|
+
def self.load( yaml_path )
|
9
|
+
flags = YAML.load( File.read( yaml_path ) )
|
10
|
+
new( flags )
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(flag_config)
|
14
|
+
flag_config ||= {}
|
15
|
+
|
16
|
+
@flags = flag_config.map do |flag_name, flag_details|
|
17
|
+
base_flag_from_config_entry(flag_name,flag_details)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def base_flag_from_config_entry( name, details )
|
24
|
+
symbolized_details = symbolize_keys( details )
|
25
|
+
BaseFlag.new(
|
26
|
+
name.to_sym,
|
27
|
+
symbolized_details.fetch( :description ,""),
|
28
|
+
symbolized_details.fetch( :default ,false)
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def symbolize_keys(hash)
|
33
|
+
hash.inject({}) do |symbolized_hash, (key, value)|
|
34
|
+
symbolized_hash[key.to_sym] = value
|
35
|
+
symbolized_hash
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module RackFlags
|
2
|
+
|
3
|
+
class CookieCodec
|
4
|
+
COOKIE_KEY='rack-flags'
|
5
|
+
|
6
|
+
class Parser
|
7
|
+
attr_reader :overrides
|
8
|
+
|
9
|
+
def self.parse(cookie_value)
|
10
|
+
parser = new
|
11
|
+
parser.parse(cookie_value)
|
12
|
+
parser.overrides
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize()
|
16
|
+
@overrides = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse(raw_overrides)
|
20
|
+
return if raw_overrides.nil?
|
21
|
+
|
22
|
+
raw_overrides.split(' ').each do |override|
|
23
|
+
parse_override(override)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
BANG_DETECTOR = Regexp.compile(/^!(.+)/)
|
30
|
+
|
31
|
+
def parse_override(override)
|
32
|
+
if override_without_bang = override[BANG_DETECTOR,1]
|
33
|
+
add_override(override_without_bang,false)
|
34
|
+
else
|
35
|
+
add_override(override,true)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_override( name, value )
|
40
|
+
@overrides[name.to_sym] = value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def overrides_from_env(env)
|
45
|
+
req = Rack::Request.new(env)
|
46
|
+
raw_overrides = req.cookies[COOKIE_KEY]
|
47
|
+
Parser.parse( raw_overrides )
|
48
|
+
end
|
49
|
+
|
50
|
+
def generate_cookie_from(overrides)
|
51
|
+
cookie_values = overrides.map {|flag_name, flag_value| cookie_value_for(flag_name, flag_value) }
|
52
|
+
cookie_values.compact.join(' ')
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def cookie_value_for(flag_name, flag_value)
|
58
|
+
case flag_value
|
59
|
+
when true then flag_name
|
60
|
+
when false then "!#{flag_name}"
|
61
|
+
else nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module RackFlags
|
2
|
+
class RackMiddleware
|
3
|
+
ENV_KEY = 'x-rack-flags.flag-reader'
|
4
|
+
|
5
|
+
def initialize( app, args )
|
6
|
+
@app = app
|
7
|
+
yaml_path = args.fetch( :yaml_path ){ raise ArgumentError.new( 'yaml_path must be provided' ) }
|
8
|
+
@config = Config.load( yaml_path )
|
9
|
+
end
|
10
|
+
|
11
|
+
def call( env )
|
12
|
+
overrides = CookieCodec.new.overrides_from_env( env )
|
13
|
+
reader = Reader.new( @config.flags, overrides )
|
14
|
+
env[ENV_KEY] = reader
|
15
|
+
|
16
|
+
@app.call(env)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module RackFlags
|
4
|
+
|
5
|
+
BaseFlag = Struct.new(:name,:description,:default)
|
6
|
+
|
7
|
+
FullFlag = Struct.new(:base_flag, :override) do
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegators :base_flag, :name, :description, :default
|
11
|
+
end
|
12
|
+
|
13
|
+
class Reader
|
14
|
+
def self.blank_reader
|
15
|
+
new( [], {} )
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize( base_flags, overrides )
|
19
|
+
@base_flags = load_base_flags( base_flags )
|
20
|
+
@overrides = overrides
|
21
|
+
end
|
22
|
+
|
23
|
+
def base_flags
|
24
|
+
@base_flags.values.inject({}) { |h,flag| h[flag.name] = flag.default; h }
|
25
|
+
end
|
26
|
+
|
27
|
+
def full_flags
|
28
|
+
@base_flags.values.map do |base_flag|
|
29
|
+
FullFlag.new(base_flag, @overrides[base_flag.name.to_sym])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def on?(flag_name)
|
34
|
+
flag_name = flag_name.to_sym
|
35
|
+
|
36
|
+
return false unless base_flag_exists?( flag_name )
|
37
|
+
|
38
|
+
@overrides.fetch(flag_name) do
|
39
|
+
# fall back to defaults
|
40
|
+
fetch_base_flag(flag_name).default
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def load_base_flags( flags )
|
47
|
+
Hash[ *flags.map{ |f| [f.name.to_sym, f] }.flatten ]
|
48
|
+
end
|
49
|
+
|
50
|
+
def base_flag_exists?( flag_name )
|
51
|
+
@base_flags.has_key?( flag_name )
|
52
|
+
end
|
53
|
+
|
54
|
+
def fetch_base_flag( flag_name )
|
55
|
+
@base_flags.fetch( flag_name ) do
|
56
|
+
BaseFlag.new( nil, nil, false ) # if we couldn't find a flag return a Null flag
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
data/lib/rack-flags.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rack-flags/reader'
|
2
|
+
require 'rack-flags/cookie_codec'
|
3
|
+
require 'rack-flags/config'
|
4
|
+
require 'rack-flags/rack_middleware'
|
5
|
+
require 'rack-flags/admin_app'
|
6
|
+
|
7
|
+
module RackFlags
|
8
|
+
def self.for_env(env)
|
9
|
+
env[RackMiddleware::ENV_KEY] || Reader.blank_reader
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.path_for_resource(subpath)
|
13
|
+
File.expand_path( File.join( "../../resources", subpath ), __FILE__ )
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>rack-flags admin</title>
|
5
|
+
<link href="<%=css_href%>" rel="stylesheet" type="text/css">
|
6
|
+
</head>
|
7
|
+
<body>
|
8
|
+
<h1>rack-flags admin</h1>
|
9
|
+
<form method="post">
|
10
|
+
<%flags.each do |flag|%>
|
11
|
+
<section data-flag-name="<%=flag.name%>">
|
12
|
+
<h3><%=flag.name%></h3>
|
13
|
+
<p><%=flag.description%></p>
|
14
|
+
<div>
|
15
|
+
<label class="default">
|
16
|
+
Default (<%=flag.default%>)
|
17
|
+
<input type="radio" name="<%=flag.name%>" value="default" <%=flag.checked_attribute_for(:default)%>/>
|
18
|
+
</label>
|
19
|
+
|
20
|
+
<label class="on">
|
21
|
+
On
|
22
|
+
<input type="radio" name="<%=flag.name%>" value="on" <%=flag.checked_attribute_for(:on)%>/>
|
23
|
+
</label>
|
24
|
+
|
25
|
+
<label class="off">
|
26
|
+
Off
|
27
|
+
<input type="radio" name="<%=flag.name%>" value="off" <%=flag.checked_attribute_for(:off)%>/>
|
28
|
+
</label>
|
29
|
+
</div>
|
30
|
+
</section>
|
31
|
+
<%end%>
|
32
|
+
<input type="submit" value="Update Flags"/>
|
33
|
+
</form>
|
34
|
+
</body>
|
35
|
+
</html>
|
@@ -0,0 +1,35 @@
|
|
1
|
+
body {
|
2
|
+
font-family: Futura, "Trebuchet MS", Arial, sans-serif;
|
3
|
+
}
|
4
|
+
|
5
|
+
section {
|
6
|
+
margin: 20px 20px 40px 20px;
|
7
|
+
padding-bottom: 10px;
|
8
|
+
background-color: lightgray;
|
9
|
+
}
|
10
|
+
section>* {
|
11
|
+
padding-left: 8px;
|
12
|
+
}
|
13
|
+
|
14
|
+
section h3 {
|
15
|
+
background-color: gray;
|
16
|
+
color: white;
|
17
|
+
font-size: 16pt;
|
18
|
+
padding-top: 5px;
|
19
|
+
padding-bottom: 5px;
|
20
|
+
margin: 0;
|
21
|
+
}
|
22
|
+
|
23
|
+
section p {
|
24
|
+
margin-top: 0;
|
25
|
+
font-style: italic;
|
26
|
+
}
|
27
|
+
|
28
|
+
input[type="submit"] {
|
29
|
+
font-family: Futura, "Trebuchet MS", Arial, sans-serif;
|
30
|
+
padding: 10px;
|
31
|
+
margin-left: 20px;
|
32
|
+
font-size: 20px;
|
33
|
+
background-color: #eee;
|
34
|
+
border: 1px solid;
|
35
|
+
}
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe 'displaying flags in admin app' do
|
4
|
+
include Capybara::DSL
|
5
|
+
|
6
|
+
let( :feature_flag_config ) do
|
7
|
+
{
|
8
|
+
on_by_default: {
|
9
|
+
'default' => true,
|
10
|
+
'description' => 'this flag on by default'
|
11
|
+
},
|
12
|
+
off_by_default: {
|
13
|
+
'default' => false,
|
14
|
+
'description' => 'this flag off by default'
|
15
|
+
}
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
before :each do
|
20
|
+
ff_config_file_contains feature_flag_config
|
21
|
+
Capybara.app = app
|
22
|
+
end
|
23
|
+
|
24
|
+
let( :app ) do
|
25
|
+
yaml_path = ff_config_file_path
|
26
|
+
Rack::Builder.new do
|
27
|
+
use RackFlags::RackMiddleware, yaml_path: yaml_path
|
28
|
+
run RackFlags::AdminApp.new
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
let(:on_flag_section){ page.find('section[data-flag-name="on_by_default"]') }
|
33
|
+
let(:off_flag_section){ page.find('section[data-flag-name="off_by_default"]') }
|
34
|
+
let(:update_button){ page.find('input[type="submit"]') }
|
35
|
+
|
36
|
+
it 'successfully GETs the admin page' do
|
37
|
+
visit '/'
|
38
|
+
status_code.should == 200
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'renders the feature flag name, default and description' do
|
42
|
+
visit '/'
|
43
|
+
|
44
|
+
on_flag_section.should_not be_nil
|
45
|
+
on_flag_section.find('h3').text.should == 'on_by_default'
|
46
|
+
on_flag_section.find('p').text.should == 'this flag on by default'
|
47
|
+
on_flag_section.find('label.default').text.should include('Default (On)')
|
48
|
+
|
49
|
+
off_flag_section.should_not be_nil
|
50
|
+
off_flag_section.find('h3').text.should == 'off_by_default'
|
51
|
+
off_flag_section.find('p').text.should == 'this flag off by default'
|
52
|
+
off_flag_section.find('label.default').text.should include('Default (Off)')
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'selects the default option if there are no cookies present' do
|
56
|
+
visit '/'
|
57
|
+
|
58
|
+
verify_flag_section( on_flag_section, :default )
|
59
|
+
verify_flag_section( off_flag_section, :default )
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'allows switching off a flag defaulted to on' do
|
63
|
+
visit '/'
|
64
|
+
|
65
|
+
on_flag_section.choose( 'Off' )
|
66
|
+
update_button.click
|
67
|
+
|
68
|
+
verify_flag_section( on_flag_section, :off )
|
69
|
+
verify_flag_section( off_flag_section, :default )
|
70
|
+
end
|
71
|
+
|
72
|
+
def verify_flag_section( section, expected_state )
|
73
|
+
case expected_state.to_sym
|
74
|
+
when :default
|
75
|
+
section.find('label.default input').should be_checked
|
76
|
+
|
77
|
+
section.find('label.on input').should_not be_checked
|
78
|
+
section.find('label.off input').should_not be_checked
|
79
|
+
when :on
|
80
|
+
section.find('label.on input').should be_checked
|
81
|
+
|
82
|
+
section.find('label.default input').should_not be_checked
|
83
|
+
section.find('label.off input').should_not be_checked
|
84
|
+
when :off
|
85
|
+
section.find('label.off input').should be_checked
|
86
|
+
|
87
|
+
section.find('label.default input').should_not be_checked
|
88
|
+
section.find('label.on input').should_not be_checked
|
89
|
+
else
|
90
|
+
raise "unrecognized state '#{expected_state}'"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
require_relative 'support/reader_app'
|
3
|
+
|
4
|
+
describe 'reading feature flags in an app' do
|
5
|
+
include Rack::Test::Methods
|
6
|
+
|
7
|
+
let( :feature_flag_config ){ {} }
|
8
|
+
let( :feature_flag_cookie ){ "" }
|
9
|
+
|
10
|
+
before :each do
|
11
|
+
ff_config_file_contains feature_flag_config
|
12
|
+
set_feature_flags_cookie feature_flag_cookie
|
13
|
+
end
|
14
|
+
|
15
|
+
let( :app ) do
|
16
|
+
yaml_path = ff_config_file_path
|
17
|
+
Rack::Builder.new do
|
18
|
+
use RackFlags::RackMiddleware, yaml_path: yaml_path
|
19
|
+
run ReaderApp
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'no base flags, no overrides' do
|
24
|
+
it 'should interpret both foo and bar as off by default' do
|
25
|
+
get '/'
|
26
|
+
last_response.body.should == 'foo is off; bar is off'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'foo defined as a base flag, defaulted to true' do
|
31
|
+
let( :feature_flag_config ) do
|
32
|
+
{
|
33
|
+
foo: { default: true }
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should interpret foo as on and bar as off' do
|
38
|
+
get '/'
|
39
|
+
last_response.body.should == 'foo is on; bar is off'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'foo defaults to false, bar defaults to true' do
|
44
|
+
let( :feature_flag_config ) do
|
45
|
+
{
|
46
|
+
foo: { default: false },
|
47
|
+
bar: { default: true }
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should interpret foo as off and bar as on' do
|
52
|
+
get '/'
|
53
|
+
last_response.body.should == 'foo is off; bar is on'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'foo and bar both default to true, bar is overridden to false' do
|
58
|
+
let( :feature_flag_config ) do
|
59
|
+
{
|
60
|
+
foo: { default: true },
|
61
|
+
bar: { default: true }
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
let( :feature_flag_cookie){ "!bar" }
|
66
|
+
|
67
|
+
it 'should interpret foo as on and bar as off' do
|
68
|
+
get '/'
|
69
|
+
last_response.body.should == 'foo is on; bar is off'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
require 'rack/test'
|
3
|
+
require 'capybara/rspec'
|
4
|
+
|
5
|
+
module ConfigFileHelper
|
6
|
+
|
7
|
+
def ff_config_file_path
|
8
|
+
ff_yaml_file.path
|
9
|
+
end
|
10
|
+
|
11
|
+
def ff_config_file_contains(config)
|
12
|
+
ff_yaml_file.write( config.to_yaml )
|
13
|
+
ff_yaml_file.flush
|
14
|
+
end
|
15
|
+
|
16
|
+
def teardown_ff_config_file_if_exists
|
17
|
+
@_ff_yaml_file.unlink if @_ff_yaml_file
|
18
|
+
end
|
19
|
+
|
20
|
+
def ff_yaml_file
|
21
|
+
@_ff_yaml_file ||= Tempfile.new('rack-flags acceptance test example config file')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module CookieHelper
|
26
|
+
def set_feature_flags_cookie ff_cookie
|
27
|
+
raw_cookie = "#{RackFlags::CookieCodec::COOKIE_KEY}=#{Rack::Utils.escape(ff_cookie)}"
|
28
|
+
set_cookie(raw_cookie)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
RSpec.configure do |config|
|
33
|
+
include ConfigFileHelper
|
34
|
+
include CookieHelper
|
35
|
+
|
36
|
+
config.after :each do
|
37
|
+
teardown_ff_config_file_if_exists
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
class ReaderApp < Sinatra::Base
|
3
|
+
enable :raise_errors
|
4
|
+
disable :show_exceptions
|
5
|
+
|
6
|
+
get "/" do
|
7
|
+
flags = RackFlags.for_env(env)
|
8
|
+
|
9
|
+
output = []
|
10
|
+
if flags.on?( :foo )
|
11
|
+
output << "foo is on"
|
12
|
+
else
|
13
|
+
output << "foo is off"
|
14
|
+
end
|
15
|
+
|
16
|
+
if flags.on?( :bar )
|
17
|
+
output << "bar is on"
|
18
|
+
else
|
19
|
+
output << "bar is off"
|
20
|
+
end
|
21
|
+
|
22
|
+
output.join("; ")
|
23
|
+
end
|
24
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
module RackFlags
|
4
|
+
|
5
|
+
describe AdminApp do
|
6
|
+
describe '#call' do
|
7
|
+
let(:fake_env) { {} }
|
8
|
+
let(:admin_app) { AdminApp.new }
|
9
|
+
|
10
|
+
context 'when the request is a get' do
|
11
|
+
let(:fake_env) { {'REQUEST_METHOD' => 'GET'} }
|
12
|
+
|
13
|
+
it 'returns a successful HTML response' do
|
14
|
+
status, headers, body = admin_app.call(fake_env)
|
15
|
+
|
16
|
+
expect(status).to eq(200)
|
17
|
+
expect(headers).to include({'Content-Type' => 'text/html'})
|
18
|
+
expect(body).to_not be_empty
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when the request is a post' do
|
24
|
+
let(:fake_env) { {'REQUEST_METHOD' => 'POST', 'SCRIPT_NAME' => 'admin-app' } }
|
25
|
+
|
26
|
+
before do
|
27
|
+
any_instance_of(Rack::Request) do |rack_request|
|
28
|
+
stub(rack_request).POST { {} }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'returns a 303 redirect response' do
|
33
|
+
status, headers , body = admin_app.call(fake_env)
|
34
|
+
|
35
|
+
expect(status).to eq(303)
|
36
|
+
expect(headers).to include({'Location' => 'admin-app'})
|
37
|
+
expect(body).to be_empty
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'sets the cookie using the post params' do
|
41
|
+
any_instance_of(Rack::Request) do |rack_request|
|
42
|
+
stub(rack_request).POST { {flag_1: 'on', flag_2: 'off', flag_3: 'default'} }
|
43
|
+
end
|
44
|
+
|
45
|
+
stub.proxy(CookieCodec).new do |cookie_codec|
|
46
|
+
mock(cookie_codec).generate_cookie_from({flag_1: true, flag_2: false, flag_3: nil}) { 'flag_1 !flag2' }
|
47
|
+
end
|
48
|
+
|
49
|
+
any_instance_of(Rack::Response) do |rack_response|
|
50
|
+
mock(rack_response).set_cookie(CookieCodec::COOKIE_KEY, 'flag_1 !flag2')
|
51
|
+
end
|
52
|
+
|
53
|
+
admin_app.call(fake_env)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'returns the finished response' do
|
57
|
+
stub.proxy(Rack::Response).new do |rack_response|
|
58
|
+
mock(rack_response).finish { 'finished rack response' }
|
59
|
+
end
|
60
|
+
|
61
|
+
response = admin_app.call(fake_env)
|
62
|
+
|
63
|
+
expect(response).to eq('finished rack response')
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'when the request is something else' do
|
69
|
+
let(:fake_env) { {'REQUEST_METHOD' => 'OTHER'} }
|
70
|
+
|
71
|
+
it 'returns a 405 method not allowed response' do
|
72
|
+
status, headers , body = admin_app.call(fake_env)
|
73
|
+
|
74
|
+
expect(status).to eq(405)
|
75
|
+
expect(headers).to be_empty
|
76
|
+
expect(body).to eq('405 - METHOD NOT ALLOWED')
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
describe FullFlagPresenter do
|
85
|
+
describe '#default' do
|
86
|
+
let(:full_flag) { FullFlag.new(base_flag, nil) }
|
87
|
+
|
88
|
+
subject(:full_flag_presenter) { FullFlagPresenter.new(full_flag) }
|
89
|
+
|
90
|
+
context 'when the default is true' do
|
91
|
+
let(:base_flag) { BaseFlag.new('name', 'description', true) }
|
92
|
+
|
93
|
+
its(:default) { should == 'On' }
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'when the default is false' do
|
97
|
+
let(:base_flag) { BaseFlag.new('name', 'description', false) }
|
98
|
+
|
99
|
+
its(:default) { should == 'Off' }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe '#checked_attributes_for' do
|
104
|
+
let(:full_flag) { FullFlag.new(mock(BaseFlag), override) }
|
105
|
+
|
106
|
+
subject(:full_flag_presenter) { FullFlagPresenter.new(full_flag) }
|
107
|
+
|
108
|
+
context 'when the default value is not overridden' do
|
109
|
+
let(:final_value) { nil }
|
110
|
+
let(:override) { nil }
|
111
|
+
|
112
|
+
specify 'only its default checked state is "checked"' do
|
113
|
+
expect(subject.checked_attribute_for(:default)).to eq('checked')
|
114
|
+
expect(subject.checked_attribute_for(:on)).to eq('')
|
115
|
+
expect(subject.checked_attribute_for(:off)).to eq('')
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'when the default value is overridden to true' do
|
120
|
+
let(:final_value) { nil }
|
121
|
+
let(:override) { true }
|
122
|
+
|
123
|
+
specify 'only its on checked state is "checked"' do
|
124
|
+
expect(subject.checked_attribute_for(:default)).to eq('')
|
125
|
+
expect(subject.checked_attribute_for(:on)).to eq('checked')
|
126
|
+
expect(subject.checked_attribute_for(:off)).to eq('')
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context 'when the default value is overridden to false' do
|
131
|
+
let(:final_value) { nil }
|
132
|
+
let(:override) { false }
|
133
|
+
|
134
|
+
specify 'only its off checked state is "checked"' do
|
135
|
+
expect(subject.checked_attribute_for(:default)).to eq('')
|
136
|
+
expect(subject.checked_attribute_for(:on)).to eq('')
|
137
|
+
expect(subject.checked_attribute_for(:off)).to eq('checked')
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
module RackFlags
|
4
|
+
|
5
|
+
describe Config do
|
6
|
+
let( :config_file ) { Tempfile.new('rack-flags-config-unit-test') }
|
7
|
+
let( :yaml ) { raise NotImplementedError }
|
8
|
+
|
9
|
+
|
10
|
+
before :each do
|
11
|
+
config_file.write( yaml )
|
12
|
+
config_file.flush
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:config){ Config.load( config_file.path ) }
|
16
|
+
|
17
|
+
after :each do
|
18
|
+
config_file.unlink
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'regular yaml' do
|
22
|
+
let(:yaml) do
|
23
|
+
<<-EOS
|
24
|
+
foo:
|
25
|
+
description: the description
|
26
|
+
default: true
|
27
|
+
bar:
|
28
|
+
description: another description
|
29
|
+
default: false
|
30
|
+
EOS
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'loads a set of BaseFlags' do
|
34
|
+
config.should have(2).flags
|
35
|
+
config.flags.map(&:name) .should =~ [:foo,:bar]
|
36
|
+
config.flags.map(&:description).should =~ ["the description","another description"]
|
37
|
+
config.flags.map(&:default).should =~ [true,false]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'empty file' do
|
42
|
+
let(:yaml){ "" }
|
43
|
+
it 'loads as an empty config' do
|
44
|
+
config.should have(0).flags
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'symbolized yaml' do
|
49
|
+
let :yaml do
|
50
|
+
<<-EOS
|
51
|
+
:foo:
|
52
|
+
:default: true
|
53
|
+
:description: a description
|
54
|
+
EOS
|
55
|
+
end
|
56
|
+
|
57
|
+
subject(:flag){ config.flags.first }
|
58
|
+
|
59
|
+
it { should_not be_nil }
|
60
|
+
its(:default) { should be_true }
|
61
|
+
its(:description) { should == "a description" }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
module RackFlags
|
4
|
+
|
5
|
+
describe CookieCodec do
|
6
|
+
let(:cookie_codec){ CookieCodec.new }
|
7
|
+
|
8
|
+
describe '#overrides_from_env' do
|
9
|
+
subject(:overrides){ cookie_codec.overrides_from_env(env) }
|
10
|
+
let(:env){ {'HTTP_COOKIE'=>cookie_header } }
|
11
|
+
let(:cookie_header){ "#{CookieCodec::COOKIE_KEY}=#{Rack::Utils.escape(ff_cookie)};foo=bar" }
|
12
|
+
|
13
|
+
context 'no cookie' do
|
14
|
+
let(:env){ {} }
|
15
|
+
it{ should == {} }
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'empty cookie' do
|
19
|
+
let(:ff_cookie){ '' }
|
20
|
+
it{ should == {} }
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'simple cookie' do
|
24
|
+
let(:ff_cookie){ 'foo bar' }
|
25
|
+
it{ should == {foo: true, bar: true} }
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'cookie with positives and negatives' do
|
29
|
+
let(:ff_cookie){ 'yes !no yes-sir !na-huh' }
|
30
|
+
it{ should == {
|
31
|
+
:yes => true,
|
32
|
+
:'yes-sir' => true,
|
33
|
+
:no => false,
|
34
|
+
:'na-huh' => false
|
35
|
+
} }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '#generate_cookie_from' do
|
40
|
+
it 'returns a simple cookie' do
|
41
|
+
admin_app_overrides = {flag_1: true}
|
42
|
+
cookie = cookie_codec.generate_cookie_from(admin_app_overrides)
|
43
|
+
|
44
|
+
expect(cookie).to eq('flag_1')
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'returns a cookie with positive and negative values' do
|
48
|
+
admin_app_overrides = {flag_1: true, flag_2: false}
|
49
|
+
cookie = cookie_codec.generate_cookie_from(admin_app_overrides)
|
50
|
+
|
51
|
+
expect(cookie).to eq('flag_1 !flag_2')
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'returns a cookie with default values' do
|
55
|
+
admin_app_overrides = {flag_1: true, flag_2: false, flag_3: nil}
|
56
|
+
cookie = cookie_codec.generate_cookie_from(admin_app_overrides)
|
57
|
+
|
58
|
+
expect(cookie).to eq('flag_1 !flag_2')
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
module RackFlags
|
4
|
+
|
5
|
+
describe RackMiddleware do
|
6
|
+
|
7
|
+
def mock_out_config_loading
|
8
|
+
stub(Config).load(anything){ OpenStruct.new( flags: {} ) }
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'raise an exception if no yaml path is provided' do
|
12
|
+
lambda{
|
13
|
+
RackMiddleware.new( :fake_app, {} )
|
14
|
+
}.should raise_error( ArgumentError, 'yaml_path must be provided' )
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'loads the config from the specified yaml file' do
|
18
|
+
mock(Config).load('some/config/path')
|
19
|
+
RackMiddleware.new( :fake_app, yaml_path: 'some/config/path' )
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '#call' do
|
23
|
+
|
24
|
+
def create_middleware( fake_app = false)
|
25
|
+
fake_app ||= Proc.new {}
|
26
|
+
|
27
|
+
RackMiddleware.new( fake_app, yaml_path: 'blah' )
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'creates a Reader using the config flags when called' do
|
31
|
+
stub(Config).load(anything){ OpenStruct.new( flags: 'fake flags from config' ) }
|
32
|
+
mock(Reader).new( 'fake flags from config', anything )
|
33
|
+
|
34
|
+
middleware = create_middleware()
|
35
|
+
middleware.call( {} )
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'adds the reader to the env' do
|
39
|
+
mock_out_config_loading
|
40
|
+
|
41
|
+
mock(Reader).new(anything,anything){ 'fake derived flags' }
|
42
|
+
|
43
|
+
middleware = create_middleware()
|
44
|
+
fake_env = {}
|
45
|
+
middleware.call( fake_env )
|
46
|
+
fake_env[RackMiddleware::ENV_KEY].should == 'fake derived flags'
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'reads overrides from cookies' do
|
50
|
+
mock_out_config_loading
|
51
|
+
|
52
|
+
fake_env = { fake: 'env' }
|
53
|
+
|
54
|
+
|
55
|
+
fake_cookie_codec = mock( Object.new )
|
56
|
+
mock(CookieCodec).new{ fake_cookie_codec }
|
57
|
+
|
58
|
+
fake_cookie_codec.overrides_from_env( fake_env )
|
59
|
+
|
60
|
+
create_middleware().call( fake_env )
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'passes the overrides into the reader' do
|
64
|
+
mock_out_config_loading
|
65
|
+
|
66
|
+
mock(CookieCodec).new{ stub!.overrides_from_env{'fake overrides'} }
|
67
|
+
mock(Reader).new( anything, 'fake overrides' )
|
68
|
+
|
69
|
+
create_middleware().call( {} )
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'passes through to downstream app' do
|
73
|
+
mock_out_config_loading
|
74
|
+
|
75
|
+
fake_app ||= Proc.new do
|
76
|
+
"downstream app response"
|
77
|
+
end
|
78
|
+
|
79
|
+
middleware = create_middleware( fake_app )
|
80
|
+
middleware_response = middleware.call( {} )
|
81
|
+
|
82
|
+
middleware_response.should == "downstream app response"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
module RackFlags
|
4
|
+
describe '.for_env' do
|
5
|
+
it 'should return the flags which the middleware stuffed in the env' do
|
6
|
+
fake_env = {RackMiddleware::ENV_KEY => 'fake flags from env'}
|
7
|
+
RackFlags.for_env(fake_env).should == 'fake flags from env'
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'returns a generic empty reader if none is in the env' do
|
11
|
+
fake_env = {}
|
12
|
+
reader = RackFlags.for_env(fake_env)
|
13
|
+
reader.should_not be_nil
|
14
|
+
reader.should respond_to(:on?)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
module RackFlags
|
4
|
+
|
5
|
+
describe Reader do
|
6
|
+
def derived_flags_final_values(derived_flags)
|
7
|
+
derived_flags.map { |flag| flag.final_value }
|
8
|
+
end
|
9
|
+
|
10
|
+
def full_flags_overrides(derived_flags)
|
11
|
+
derived_flags.map { |flag| flag.override }
|
12
|
+
end
|
13
|
+
|
14
|
+
let( :base_flags ) do
|
15
|
+
[
|
16
|
+
BaseFlag.new( :usually_on, 'a flag', true ),
|
17
|
+
BaseFlag.new( :usually_off, 'another flag', false )
|
18
|
+
]
|
19
|
+
end
|
20
|
+
let( :overrides ){ {} }
|
21
|
+
|
22
|
+
subject( :reader ){ Reader.new( base_flags, overrides ) }
|
23
|
+
|
24
|
+
shared_examples 'full flags that mimic the base flags' do
|
25
|
+
it 'has a name, description, and default for each base flag' do
|
26
|
+
full_flags = subject.full_flags
|
27
|
+
|
28
|
+
expect(full_flags.length).to eq(base_flags.length)
|
29
|
+
|
30
|
+
expect(full_flags[0].name).to eq(:usually_on)
|
31
|
+
expect(full_flags[0].description).to eq('a flag')
|
32
|
+
expect(full_flags[0].default).to be_true
|
33
|
+
|
34
|
+
expect(full_flags[1].name).to eq(:usually_off)
|
35
|
+
expect(full_flags[1].description).to eq('another flag')
|
36
|
+
expect(full_flags[1].default).to be_false
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'no overrides' do
|
42
|
+
let( :overrides ){ {} }
|
43
|
+
|
44
|
+
its(:base_flags){ should == {usually_on: true, usually_off: false} }
|
45
|
+
|
46
|
+
specify 'on? is true for a flag which is on by default' do
|
47
|
+
subject.on?( :usually_on ).should be_true
|
48
|
+
end
|
49
|
+
|
50
|
+
specify 'on? is false for a flag which is off by default' do
|
51
|
+
subject.on?( :usually_off ).should be_false
|
52
|
+
end
|
53
|
+
|
54
|
+
specify 'on? is false for unknown flags' do
|
55
|
+
subject.on?( :unknown_flag ).should be_false
|
56
|
+
end
|
57
|
+
|
58
|
+
it_behaves_like 'full flags that mimic the base flags'
|
59
|
+
|
60
|
+
it 'has derived flags with nil override values' do
|
61
|
+
overrides = full_flags_overrides(subject.full_flags)
|
62
|
+
|
63
|
+
expect(overrides).to eq([nil, nil])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'overridden to false' do
|
68
|
+
let( :overrides ){ {usually_on: false} }
|
69
|
+
|
70
|
+
specify 'on? is false' do
|
71
|
+
subject.on?( :usually_on ).should be_false
|
72
|
+
end
|
73
|
+
|
74
|
+
it_behaves_like 'full flags that mimic the base flags'
|
75
|
+
|
76
|
+
it 'has full flags with one override value' do
|
77
|
+
overrides = full_flags_overrides(subject.full_flags)
|
78
|
+
|
79
|
+
expect(overrides).to eq([false, nil])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'overridding a flag which has no base' do
|
84
|
+
let( :overrides ){ {no_base: true} }
|
85
|
+
|
86
|
+
specify 'on? is false' do
|
87
|
+
subject.on?( :no_base ).should be_false
|
88
|
+
end
|
89
|
+
|
90
|
+
it_behaves_like 'full flags that mimic the base flags'
|
91
|
+
|
92
|
+
it 'has full flags with no override values' do
|
93
|
+
overrides = full_flags_overrides(subject.full_flags)
|
94
|
+
|
95
|
+
expect(overrides).to eq([nil, nil])
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative '../spec_helper'
|
metadata
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-flags
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Pete Hodgson
|
9
|
+
- Ryan Oglesby
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-07-14 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rake
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: rack
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ~>
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '1.4'
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ~>
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.4'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: sinatra
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.3'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ~>
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '1.3'
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: pry-debugger
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
type: :development
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
- !ruby/object:Gem::Dependency
|
80
|
+
name: rspec-core
|
81
|
+
requirement: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
type: :development
|
88
|
+
prerelease: false
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: rspec-expectations
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ! '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rr
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ! '>='
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
- !ruby/object:Gem::Dependency
|
128
|
+
name: capybara
|
129
|
+
requirement: !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
131
|
+
requirements:
|
132
|
+
- - ! '>='
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
type: :development
|
136
|
+
prerelease: false
|
137
|
+
version_requirements: !ruby/object:Gem::Requirement
|
138
|
+
none: false
|
139
|
+
requirements:
|
140
|
+
- - ! '>='
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
description: This is a simple lightweight way to expose work-in-progress functionality
|
144
|
+
to developers, testers or other internal users.
|
145
|
+
email:
|
146
|
+
- git@thepete.net
|
147
|
+
executables: []
|
148
|
+
extensions: []
|
149
|
+
extra_rdoc_files: []
|
150
|
+
files:
|
151
|
+
- lib/rack-flags.rb
|
152
|
+
- lib/rack-flags/admin_app.rb
|
153
|
+
- lib/rack-flags/config.rb
|
154
|
+
- lib/rack-flags/cookie_codec.rb
|
155
|
+
- lib/rack-flags/rack_middleware.rb
|
156
|
+
- lib/rack-flags/reader.rb
|
157
|
+
- lib/rack-flags/version.rb
|
158
|
+
- resources/admin_app/index.html.erb
|
159
|
+
- resources/admin_app/style.css
|
160
|
+
- spec/acceptance/administering_feature_flags_spec.rb
|
161
|
+
- spec/acceptance/reading_feature_flags_spec.rb
|
162
|
+
- spec/acceptance/spec_helper.rb
|
163
|
+
- spec/acceptance/support/reader_app.rb
|
164
|
+
- spec/spec_helper.rb
|
165
|
+
- spec/unit/admin_app_spec.rb
|
166
|
+
- spec/unit/config_spec.rb
|
167
|
+
- spec/unit/cookie_codec_spec.rb
|
168
|
+
- spec/unit/middleware_spec.rb
|
169
|
+
- spec/unit/rack_flags_module_methods_spec.rb
|
170
|
+
- spec/unit/reader_spec.rb
|
171
|
+
- spec/unit/spec_helper.rb
|
172
|
+
homepage: https://github.com/moredip/rack-flags
|
173
|
+
licenses: []
|
174
|
+
post_install_message:
|
175
|
+
rdoc_options: []
|
176
|
+
require_paths:
|
177
|
+
- lib
|
178
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
179
|
+
none: false
|
180
|
+
requirements:
|
181
|
+
- - ! '>='
|
182
|
+
- !ruby/object:Gem::Version
|
183
|
+
version: '0'
|
184
|
+
segments:
|
185
|
+
- 0
|
186
|
+
hash: -2280499656748688466
|
187
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
188
|
+
none: false
|
189
|
+
requirements:
|
190
|
+
- - ! '>='
|
191
|
+
- !ruby/object:Gem::Version
|
192
|
+
version: '0'
|
193
|
+
segments:
|
194
|
+
- 0
|
195
|
+
hash: -2280499656748688466
|
196
|
+
requirements: []
|
197
|
+
rubyforge_project:
|
198
|
+
rubygems_version: 1.8.25
|
199
|
+
signing_key:
|
200
|
+
specification_version: 3
|
201
|
+
summary: Simple cookie-based feature flags using Rack.
|
202
|
+
test_files:
|
203
|
+
- spec/acceptance/administering_feature_flags_spec.rb
|
204
|
+
- spec/acceptance/reading_feature_flags_spec.rb
|
205
|
+
- spec/acceptance/spec_helper.rb
|
206
|
+
- spec/acceptance/support/reader_app.rb
|
207
|
+
- spec/spec_helper.rb
|
208
|
+
- spec/unit/admin_app_spec.rb
|
209
|
+
- spec/unit/config_spec.rb
|
210
|
+
- spec/unit/cookie_codec_spec.rb
|
211
|
+
- spec/unit/middleware_spec.rb
|
212
|
+
- spec/unit/rack_flags_module_methods_spec.rb
|
213
|
+
- spec/unit/reader_spec.rb
|
214
|
+
- spec/unit/spec_helper.rb
|