rack-flags 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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