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.
@@ -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
@@ -0,0 +1,3 @@
1
+ module RackFlags
2
+ VERSION = "0.1.2"
3
+ 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
@@ -0,0 +1,11 @@
1
+ require 'rr'
2
+ require 'pry'
3
+ require 'rack'
4
+
5
+ require_relative '../lib/rack-flags'
6
+
7
+ RSpec.configure do |config|
8
+ config.mock_with :rr
9
+ # or if that doesn't work due to a version incompatibility
10
+ # config.mock_with RR::Adapters::Rspec
11
+ end
@@ -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