rack-flags 0.1.2
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.
- 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
|