flipper 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +25 -1
  3. data/.github/workflows/examples.yml +7 -1
  4. data/Changelog.md +1 -638
  5. data/Gemfile +5 -1
  6. data/README.md +21 -21
  7. data/Rakefile +2 -2
  8. data/exe/flipper +5 -0
  9. data/flipper.gemspec +6 -2
  10. data/lib/flipper/adapters/http/client.rb +25 -16
  11. data/lib/flipper/adapters/strict.rb +11 -8
  12. data/lib/flipper/cli.rb +240 -0
  13. data/lib/flipper/cloud/configuration.rb +7 -1
  14. data/lib/flipper/cloud/middleware.rb +5 -5
  15. data/lib/flipper/cloud/telemetry/submitter.rb +2 -2
  16. data/lib/flipper/cloud.rb +1 -1
  17. data/lib/flipper/engine.rb +32 -17
  18. data/lib/flipper/instrumentation/log_subscriber.rb +12 -3
  19. data/lib/flipper/metadata.rb +3 -1
  20. data/lib/flipper/test_help.rb +36 -0
  21. data/lib/flipper/version.rb +11 -1
  22. data/lib/generators/flipper/setup_generator.rb +63 -0
  23. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  24. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  25. data/lib/generators/flipper/update_generator.rb +35 -0
  26. data/spec/fixtures/environment.rb +1 -0
  27. data/spec/flipper/adapter_builder_spec.rb +1 -2
  28. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  29. data/spec/flipper/adapters/http_spec.rb +92 -75
  30. data/spec/flipper/adapters/strict_spec.rb +11 -9
  31. data/spec/flipper/cli_spec.rb +164 -0
  32. data/spec/flipper/cloud/configuration_spec.rb +9 -2
  33. data/spec/flipper/cloud/dsl_spec.rb +5 -5
  34. data/spec/flipper/cloud/middleware_spec.rb +8 -8
  35. data/spec/flipper/cloud/telemetry/submitter_spec.rb +24 -24
  36. data/spec/flipper/cloud/telemetry_spec.rb +1 -1
  37. data/spec/flipper/cloud_spec.rb +4 -4
  38. data/spec/flipper/engine_spec.rb +76 -11
  39. data/spec/flipper/instrumentation/log_subscriber_spec.rb +9 -2
  40. data/spec/flipper_spec.rb +1 -1
  41. data/spec/spec_helper.rb +1 -0
  42. data/spec/support/spec_helpers.rb +10 -4
  43. data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
  44. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  45. data/test_rails/helper.rb +19 -2
  46. data/test_rails/system/test_help_test.rb +46 -0
  47. metadata +25 -8
data/README.md CHANGED
@@ -4,14 +4,14 @@
4
4
 
5
5
  # Flipper
6
6
 
7
- > Beautiful, performant feature flags for Ruby.
7
+ > Beautiful, performant feature flags for Ruby and Rails.
8
8
 
9
9
  Flipper gives you control over who has access to features in your app.
10
10
 
11
- * Enable or disable features for everyone, specific actors, groups of actors, a percentage of actors, or a percentage of time.
12
- * Configure your feature flags from the console or a web UI.
13
- * Regardless of what data store you are using, Flipper can performantly store your feature flags.
14
- * Use [Flipper Cloud](#flipper-cloud) to cascade features from multiple environments, share settings with your team, control permissions, keep an audit history, and rollback.
11
+ - Enable or disable features for everyone, specific actors, groups of actors, a percentage of actors, or a percentage of time.
12
+ - Configure your feature flags from the console or a web UI.
13
+ - Regardless of what data store you are using, Flipper can performantly store your feature flags.
14
+ - Use [Flipper Cloud](#flipper-cloud) to cascade features from multiple environments, share settings with your team, control permissions, keep an audit history, and rollback.
15
15
 
16
16
  Control your software — don't let it control you.
17
17
 
@@ -72,13 +72,13 @@ Read more about [getting started with Flipper](https://flippercloud.io/docs?utm_
72
72
 
73
73
  Like Flipper and want more? Check out [Flipper Cloud](https://www.flippercloud.io?utm_source=oss&utm_medium=readme&utm_campaign=check_out), which comes with:
74
74
 
75
- * **multiple environments** — production, staging, per continent, whatever you need. Every environment inherits from production by default and every project comes with a [project overview page](https://blog.flippercloud.io/project-overview/) that shows each feature and its status in each environment.
76
- * **personal environments** — everyone on your team gets a personal environment (that inherits from production) which they can modify however they want without stepping on anyone else's toes.
77
- * **permissions** — grant access to everyone in your organization or lockdown each project to particular people. You can even limit access to a particular environment (like production) to specific people.
78
- * **audit history** — every feature change and who made it.
79
- * **rollbacks** — enable or disable a feature accidentally? No problem. You can roll back to any point in the audit history with a single click.
80
- * **maintenance** — we'll keep the lights on for you. We also have handy webhooks and background polling for keeping your app in sync with Cloud, so **our availability won't affect yours**. All your feature flag reads are local to your app.
81
- * **everything in one place** — no need to bounce around from different application UIs or IRB consoles.
75
+ - **multiple environments** — production, staging, per continent, whatever you need. Every environment inherits from production by default and every project comes with a [project overview page](https://blog.flippercloud.io/project-overview/) that shows each feature and its status in each environment.
76
+ - **personal environments** — everyone on your team gets a personal environment (that inherits from production) which they can modify however they want without stepping on anyone else's toes.
77
+ - **permissions** — grant access to everyone in your organization or lockdown each project to particular people. You can even limit access to a particular environment (like production) to specific people.
78
+ - **audit history** — every feature change and who made it.
79
+ - **rollbacks** — enable or disable a feature accidentally? No problem. You can roll back to any point in the audit history with a single click.
80
+ - **maintenance** — we'll keep the lights on for you. We also have handy webhooks and background polling for keeping your app in sync with Cloud, so **our availability won't affect yours**. All your feature flag reads are local to your app.
81
+ - **everything in one place** — no need to bounce around from different application UIs or IRB consoles.
82
82
 
83
83
  [![Flipper Cloud Screenshot](docs/images/flipper_cloud.png)](https://www.flippercloud.io?utm_source=oss&utm_medium=readme&utm_campaign=screenshot)
84
84
 
@@ -99,15 +99,15 @@ We also have a [free plan](https://www.flippercloud.io?utm_source=oss&utm_medium
99
99
 
100
100
  1. Update the version to be whatever it should be and commit.
101
101
  2. `script/release`
102
- 3. Profit.
102
+ 3. Create a new [GitHub Release](https://github.com/flippercloud/flipper/releases/new)
103
103
 
104
104
  ## Brought To You By
105
105
 
106
- | pic | @mention | area |
107
- |---|---|---|
108
- | ![@jnunemaker](https://avatars3.githubusercontent.com/u/235?s=64) | [@jnunemaker](https://github.com/jnunemaker) | most things |
109
- | ![@bkeepers](https://avatars3.githubusercontent.com/u/173?s=64) | [@bkeepers](https://github.com/bkeepers) | most things |
110
- | ![@dpep](https://avatars3.githubusercontent.com/u/918804?s=64) | [@dpep](https://github.com/dpep) | tbd |
111
- | ![@alexwheeler](https://avatars3.githubusercontent.com/u/3260042?s=64) | [@alexwheeler](https://github.com/alexwheeler) | api |
112
- | ![@thetimbanks](https://avatars1.githubusercontent.com/u/471801?s=64) | [@thetimbanks](https://github.com/thetimbanks) | ui |
113
- | ![@lazebny](https://avatars1.githubusercontent.com/u/6276766?s=64) | [@lazebny](https://github.com/lazebny) | docker |
106
+ | pic | @mention | area |
107
+ | ---------------------------------------------------------------------- | ---------------------------------------------- | ----------- |
108
+ | ![@jnunemaker](https://avatars3.githubusercontent.com/u/235?s=64) | [@jnunemaker](https://github.com/jnunemaker) | most things |
109
+ | ![@bkeepers](https://avatars3.githubusercontent.com/u/173?s=64) | [@bkeepers](https://github.com/bkeepers) | most things |
110
+ | ![@dpep](https://avatars3.githubusercontent.com/u/918804?s=64) | [@dpep](https://github.com/dpep) | tbd |
111
+ | ![@alexwheeler](https://avatars3.githubusercontent.com/u/3260042?s=64) | [@alexwheeler](https://github.com/alexwheeler) | api |
112
+ | ![@thetimbanks](https://avatars1.githubusercontent.com/u/471801?s=64) | [@thetimbanks](https://github.com/thetimbanks) | ui |
113
+ | ![@lazebny](https://avatars1.githubusercontent.com/u/6276766?s=64) | [@lazebny](https://github.com/lazebny) | docker |
data/Rakefile CHANGED
@@ -27,7 +27,8 @@ end
27
27
 
28
28
  require 'rspec/core/rake_task'
29
29
  RSpec::Core::RakeTask.new(:spec) do |t|
30
- t.rspec_opts = %w(--color --format documentation)
30
+ t.rspec_opts = %w(--color)
31
+ t.verbose = false
31
32
  end
32
33
 
33
34
  namespace :spec do
@@ -41,7 +42,6 @@ end
41
42
  Rake::TestTask.new do |t|
42
43
  t.libs = %w(lib test)
43
44
  t.pattern = 'test/**/*_test.rb'
44
- t.options = '--documentation'
45
45
  t.warning = false
46
46
  end
47
47
 
data/exe/flipper ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "flipper/cli"
4
+
5
+ Flipper::CLI.run(ARGV)
data/flipper.gemspec CHANGED
@@ -6,7 +6,7 @@ plugin_files = []
6
6
  plugin_test_files = []
7
7
 
8
8
  Dir['flipper-*.gemspec'].map do |gemspec|
9
- spec = eval(File.read(gemspec))
9
+ spec = Gem::Specification.load(gemspec)
10
10
  plugin_files << spec.files
11
11
  plugin_test_files << spec.files
12
12
  end
@@ -23,10 +23,12 @@ ignored_test_files.flatten!.uniq!
23
23
  Gem::Specification.new do |gem|
24
24
  gem.authors = ['John Nunemaker']
25
25
  gem.email = 'support@flippercloud.io'
26
- gem.summary = 'Beautiful, performant feature flags for Ruby.'
26
+ gem.summary = 'Beautiful, performant feature flags for Ruby and Rails.'
27
27
  gem.homepage = 'https://www.flippercloud.io/docs'
28
28
  gem.license = 'MIT'
29
29
 
30
+ gem.bindir = "exe"
31
+ gem.executables = `git ls-files -- exe/*`.split("\n").map { |f| File.basename(f) }
30
32
  gem.files = `git ls-files`.split("\n") - ignored_files + ['lib/flipper/version.rb']
31
33
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - ignored_test_files
32
34
  gem.name = 'flipper'
@@ -35,4 +37,6 @@ Gem::Specification.new do |gem|
35
37
  gem.metadata = Flipper::METADATA
36
38
 
37
39
  gem.add_dependency 'concurrent-ruby', '< 2'
40
+
41
+ gem.required_ruby_version = ">= #{Flipper::REQUIRED_RUBY_VERSION}"
38
42
  end
@@ -7,26 +7,28 @@ module Flipper
7
7
  class Http
8
8
  class Client
9
9
  DEFAULT_HEADERS = {
10
- 'Content-Type' => 'application/json',
11
- 'Accept' => 'application/json',
12
- 'User-Agent' => "Flipper HTTP Adapter v#{VERSION}",
10
+ 'content-type' => 'application/json',
11
+ 'accept' => 'application/json',
12
+ 'user-agent' => "Flipper HTTP Adapter v#{VERSION}",
13
13
  }.freeze
14
14
 
15
15
  HTTPS_SCHEME = "https".freeze
16
16
 
17
17
  CLIENT_FRAMEWORKS = {
18
- rails: -> { Rails.version if defined?(Rails) },
19
- sinatra: -> { Sinatra::VERSION if defined?(Sinatra) },
20
- hanami: -> { Hanami::VERSION if defined?(Hanami) },
18
+ rails: -> { Rails.version if defined?(Rails) },
19
+ sinatra: -> { Sinatra::VERSION if defined?(Sinatra) },
20
+ hanami: -> { Hanami::VERSION if defined?(Hanami) },
21
+ sidekiq: -> { Sidekiq::VERSION if defined?(Sidekiq) },
22
+ good_job: -> { GoodJob::VERSION if defined?(GoodJob) },
21
23
  }
22
24
 
23
25
  attr_reader :uri, :headers
24
26
  attr_reader :basic_auth_username, :basic_auth_password
25
- attr_reader :read_timeout, :open_timeout, :write_timeout, :max_retries, :debug_output
27
+ attr_reader :read_timeout, :open_timeout, :write_timeout
28
+ attr_reader :max_retries, :debug_output
26
29
 
27
30
  def initialize(options = {})
28
31
  @uri = URI(options.fetch(:url))
29
- @headers = DEFAULT_HEADERS.merge(options[:headers] || {})
30
32
  @basic_auth_username = options[:basic_auth_username]
31
33
  @basic_auth_password = options[:basic_auth_password]
32
34
  @read_timeout = options[:read_timeout]
@@ -34,9 +36,16 @@ module Flipper
34
36
  @write_timeout = options[:write_timeout]
35
37
  @max_retries = options.key?(:max_retries) ? options[:max_retries] : 0
36
38
  @debug_output = options[:debug_output]
39
+
40
+ @headers = {}
41
+ DEFAULT_HEADERS.each { |key, value| add_header key, value }
42
+ if options[:headers]
43
+ options[:headers].each { |key, value| add_header key, value }
44
+ end
37
45
  end
38
46
 
39
47
  def add_header(key, value)
48
+ key = key.to_s.downcase.gsub('_'.freeze, '-'.freeze).freeze
40
49
  @headers[key] = value
41
50
  end
42
51
 
@@ -87,13 +96,13 @@ module Flipper
87
96
 
88
97
  def build_request(http_method, uri, headers, options)
89
98
  request_headers = {
90
- client_language: "ruby",
91
- client_language_version: "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
92
- client_platform: RUBY_PLATFORM,
93
- client_engine: defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
94
- client_pid: Process.pid.to_s,
95
- client_thread: Thread.current.object_id.to_s,
96
- client_hostname: Socket.gethostname,
99
+ 'client-language' => "ruby",
100
+ 'client-language-version' => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
101
+ 'client-platform' => RUBY_PLATFORM,
102
+ 'client-engine' => defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
103
+ 'client-pid' => Process.pid.to_s,
104
+ 'client-thread' => Thread.current.object_id.to_s,
105
+ 'client-hostname' => Socket.gethostname,
97
106
  }.merge(headers)
98
107
 
99
108
  body = options[:body]
@@ -101,7 +110,7 @@ module Flipper
101
110
  request.initialize_http_header(request_headers)
102
111
 
103
112
  client_frameworks.each do |framework, version|
104
- request.add_field("Client-Framework", [framework, version].join("="))
113
+ request.add_field("client-framework", [framework, version].join("="))
105
114
  end
106
115
 
107
116
  request.body = body if body
@@ -12,18 +12,12 @@ module Flipper
12
12
  end
13
13
  end
14
14
 
15
- HANDLERS = {
16
- raise: ->(feature) { raise NotFound.new(feature.key) },
17
- warn: ->(feature) { warn NotFound.new(feature.key).message },
18
- noop: ->(_) { },
19
- }
20
-
21
15
  def_delegators :@adapter, :features, :get_all, :add, :remove, :clear, :enable, :disable
22
16
 
23
17
  def initialize(adapter, handler = nil, &block)
24
18
  @name = :strict
25
19
  @adapter = adapter
26
- @handler = block || HANDLERS.fetch(handler)
20
+ @handler = block || handler
27
21
  end
28
22
 
29
23
  def get(feature)
@@ -39,7 +33,16 @@ module Flipper
39
33
  private
40
34
 
41
35
  def assert_feature_exists(feature)
42
- @handler.call(feature) unless @adapter.features.include?(feature.key)
36
+ return if @adapter.features.include?(feature.key)
37
+
38
+ case handler
39
+ when Proc then handler.call(feature)
40
+ when :warn then warn NotFound.new(feature.key).message
41
+ when :noop, false, nil
42
+ # noop
43
+ else # truthy or :raise
44
+ raise NotFound.new(feature.key)
45
+ end
43
46
  end
44
47
 
45
48
  end
@@ -0,0 +1,240 @@
1
+ require 'optparse'
2
+
3
+ module Flipper
4
+ class CLI < OptionParser
5
+ def self.run(argv = ARGV)
6
+ new.run(argv)
7
+ end
8
+
9
+ # Path to the local Rails application's environment configuration.
10
+ DEFAULT_REQUIRE = "./config/environment"
11
+
12
+ def initialize
13
+ super
14
+
15
+ # Program is always flipper, no matter how it's invoked
16
+ @program_name = 'flipper'
17
+ @require = ENV.fetch("FLIPPER_REQUIRE", DEFAULT_REQUIRE)
18
+ @commands = {}
19
+
20
+ %w[enable disable].each do |action|
21
+ command action do |c|
22
+ c.banner = "Usage: #{c.program_name} [options] <feature>"
23
+ c.description = "#{action.to_s.capitalize} a feature"
24
+
25
+ values = []
26
+
27
+ c.on('-a id', '--actor=id', "#{action} for an actor") do |id|
28
+ values << Actor.new(id)
29
+ end
30
+ c.on('-g name', '--group=name', "#{action} for a group") do |name|
31
+ values << Types::Group.new(name)
32
+ end
33
+ c.on('-p NUM', '--percentage-of-actors=NUM', Numeric, "#{action} for a percentage of actors") do |num|
34
+ values << Types::PercentageOfActors.new(num)
35
+ end
36
+ c.on('-t NUM', '--percentage-of-time=NUM', Numeric, "#{action} for a percentage of time") do |num|
37
+ values << Types::PercentageOfTime.new(num)
38
+ end
39
+ c.on('-x expressions', '--expression=NUM', "#{action} for the given expression") do |expression|
40
+ begin
41
+ values << Flipper::Expression.build(JSON.parse(expression))
42
+ rescue JSON::ParserError => e
43
+ warn "JSON parse error: #{e.message}"
44
+ exit 1
45
+ rescue ArgumentError => e
46
+ warn "Invalid expression: #{e.message}"
47
+ exit 1
48
+ end
49
+ end
50
+
51
+ c.action do |feature|
52
+ f = Flipper.feature(feature)
53
+
54
+ if values.empty?
55
+ f.send(action)
56
+ else
57
+ values.each { |value| f.send(action, value) }
58
+ end
59
+
60
+ puts feature_details(f)
61
+ end
62
+ end
63
+ end
64
+
65
+ command 'list' do |c|
66
+ c.description = "List defined features"
67
+ c.action do
68
+ puts feature_summary(Flipper.features)
69
+ end
70
+ end
71
+
72
+ command 'show' do |c|
73
+ c.description = "Show a defined feature"
74
+ c.action do |feature|
75
+ puts feature_details(Flipper.feature(feature))
76
+ end
77
+ end
78
+
79
+ command 'help' do |c|
80
+ c.load_environment = false
81
+ c.action do |command = nil|
82
+ puts command ? @commands[command].help : help
83
+ end
84
+ end
85
+
86
+ on_tail('-r path', "The path to load your application. Default: #{@require}") do |path|
87
+ @require = path
88
+ end
89
+
90
+ # Options available on all commands
91
+ on_tail('-h', '--help', 'Print help message') do
92
+ puts help
93
+ exit
94
+ end
95
+
96
+ # Set help documentation
97
+ self.banner = "Usage: #{program_name} [options] <command>"
98
+ separator ""
99
+ separator "Commands:"
100
+
101
+ pad = @commands.keys.map(&:length).max + 2
102
+ @commands.each do |name, command|
103
+ separator " #{name.to_s.ljust(pad, " ")} #{command.description}" if command.description
104
+ end
105
+
106
+ separator ""
107
+ separator "Options:"
108
+ end
109
+
110
+ def run(argv)
111
+ command, *args = order(argv)
112
+
113
+ if @commands[command]
114
+ load_environment! if @commands[command].load_environment
115
+ @commands[command].run(args)
116
+ else
117
+ puts help
118
+
119
+ if command
120
+ warn "Unknown command: #{command}"
121
+ exit 1
122
+ end
123
+ end
124
+ rescue OptionParser::InvalidOption => e
125
+ warn e.message
126
+ exit 1
127
+ end
128
+
129
+ # Helper method to define a new command
130
+ def command(name, &block)
131
+ @commands[name] = Command.new(program_name: "#{program_name} #{name}")
132
+ block.call(@commands[name])
133
+ end
134
+
135
+ def load_environment!
136
+ ENV["FLIPPER_CLOUD_LOGGING_ENABLED"] ||= "false"
137
+ require File.expand_path(@require)
138
+ # Ensure all of flipper gets loaded if it hasn't already.
139
+ require 'flipper'
140
+ rescue LoadError => e
141
+ warn e.message
142
+ exit 1
143
+ end
144
+
145
+ def feature_summary(features)
146
+ features = Array(features)
147
+ padding = features.map { |f| f.key.to_s.length }.max
148
+
149
+ features.map do |feature|
150
+ summary = case feature.state
151
+ when :on
152
+ colorize("⏺ enabled", [:GREEN])
153
+ when :off
154
+ "⦸ disabled"
155
+ else
156
+ "#{colorize("◯ enabled", [:YELLOW])} for " + feature.enabled_gates.map do |gate|
157
+ case gate.name
158
+ when :actor
159
+ pluralize feature.actors_value.size, 'actor', 'actors'
160
+ when :group
161
+ pluralize feature.groups_value.size, 'group', 'groups'
162
+ when :percentage_of_actors
163
+ "#{feature.percentage_of_actors_value}% of actors"
164
+ when :percentage_of_time
165
+ "#{feature.percentage_of_time_value}% of time"
166
+ when :expression
167
+ "an expression"
168
+ end
169
+ end.join(', ')
170
+ end
171
+
172
+ colorize("%-#{padding}s" % feature.key, [:BOLD, :WHITE]) + " is #{summary}"
173
+ end
174
+ end
175
+
176
+ def feature_details(feature)
177
+ summary = case feature.state
178
+ when :on
179
+ colorize("⏺ enabled", [:GREEN])
180
+ when :off
181
+ "⦸ disabled"
182
+ else
183
+ lines = feature.enabled_gates.map do |gate|
184
+ case gate.name
185
+ when :actor
186
+ [ pluralize(feature.actors_value.size, 'actor', 'actors') ] +
187
+ feature.actors_value.map { |actor| "- #{actor}" }
188
+ when :group
189
+ [ pluralize(feature.groups_value.size, 'group', 'groups') ] +
190
+ feature.groups_value.map { |group| " - #{group}" }
191
+ when :percentage_of_actors
192
+ "#{feature.percentage_of_actors_value}% of actors"
193
+ when :percentage_of_time
194
+ "#{feature.percentage_of_time_value}% of time"
195
+ when :expression
196
+ json = indent(JSON.pretty_generate(feature.expression_value), 2)
197
+ "the expression: \n#{colorize(json, [:MAGENTA])}"
198
+ end
199
+ end
200
+
201
+ "#{colorize("◯ conditionally enabled", [:YELLOW])} for:\n" +
202
+ indent(lines.flatten.join("\n"), 2)
203
+ end
204
+
205
+ "#{colorize(feature.key, [:BOLD, :WHITE])} is #{summary}"
206
+ end
207
+
208
+ def pluralize(count, singular, plural)
209
+ "#{count} #{count == 1 ? singular : plural}"
210
+ end
211
+
212
+ def colorize(text, options)
213
+ IRB::Color.colorize(text, options)
214
+ end
215
+
216
+ def indent(text, spaces)
217
+ text.gsub(/^/, " " * spaces)
218
+ end
219
+
220
+ class Command < OptionParser
221
+ attr_accessor :description, :load_environment
222
+
223
+ def initialize(program_name: nil)
224
+ super()
225
+ @program_name = program_name
226
+ @load_environment = true
227
+ @action = lambda { }
228
+ end
229
+
230
+ def run(argv)
231
+ # Parse argv and call action with arguments
232
+ @action.call(*permute(argv))
233
+ end
234
+
235
+ def action(&block)
236
+ @action = block
237
+ end
238
+ end
239
+ end
240
+ end
@@ -163,7 +163,8 @@ module Flipper
163
163
  max_retries: 0, # we'll handle retries ourselves
164
164
  debug_output: @debug_output,
165
165
  headers: {
166
- "Flipper-Cloud-Token" => @token,
166
+ "flipper-cloud-token" => @token,
167
+ "accept-encoding" => "gzip",
167
168
  },
168
169
  })
169
170
  end
@@ -186,6 +187,11 @@ module Flipper
186
187
  def setup_http(options)
187
188
  set_option :url, options, default: DEFAULT_URL
188
189
  set_option :debug_output, options, from_env: false
190
+
191
+ if @debug_output.nil? && Flipper::Typecast.to_boolean(ENV["FLIPPER_CLOUD_DEBUG_OUTPUT_STDOUT"])
192
+ @debug_output = STDOUT
193
+ end
194
+
189
195
  set_option :read_timeout, options, default: 5, typecast: :float, minimum: 0.1
190
196
  set_option :open_timeout, options, default: 2, typecast: :float, minimum: 0.1
191
197
  set_option :write_timeout, options, default: 5, typecast: :float, minimum: 0.1
@@ -24,7 +24,7 @@ module Flipper
24
24
  if request.post? && (request.path_info.match(ROOT_PATH) || request.path_info.match(WEBHOOK_PATH))
25
25
  status = 200
26
26
  headers = {
27
- "content-type" => "application/json",
27
+ Rack::CONTENT_TYPE => "application/json",
28
28
  }
29
29
  body = "{}"
30
30
  payload = request.body.read
@@ -41,12 +41,12 @@ module Flipper
41
41
  })
42
42
  rescue Flipper::Adapters::Http::Error => error
43
43
  status = error.response.code.to_i == 402 ? 402 : 500
44
- headers["Flipper-Cloud-Response-Error-Class"] = error.class.name
45
- headers["Flipper-Cloud-Response-Error-Message"] = error.message
44
+ headers["flipper-cloud-response-error-class"] = error.class.name
45
+ headers["flipper-cloud-response-error-message"] = error.message
46
46
  rescue => error
47
47
  status = 500
48
- headers["Flipper-Cloud-Response-Error-Class"] = error.class.name
49
- headers["Flipper-Cloud-Response-Error-Message"] = error.message
48
+ headers["flipper-cloud-response-error-class"] = error.class.name
49
+ headers["flipper-cloud-response-error-message"] = error.message
50
50
  end
51
51
  end
52
52
  rescue MessageVerifier::InvalidSignature
@@ -78,8 +78,8 @@ module Flipper
78
78
 
79
79
  def submit(body)
80
80
  client = @cloud_configuration.http_client
81
- client.add_header :schema_version, SCHEMA_VERSION
82
- client.add_header :content_encoding, GZIP_ENCODING
81
+ client.add_header "schema-version", SCHEMA_VERSION
82
+ client.add_header "content-encoding", GZIP_ENCODING
83
83
 
84
84
  response = client.post PATH, body
85
85
  code = response.code.to_i
data/lib/flipper/cloud.rb CHANGED
@@ -24,7 +24,7 @@ module Flipper
24
24
  env_key = options.fetch(:env_key, 'flipper')
25
25
  memoizer_options = options.fetch(:memoizer_options, {})
26
26
 
27
- app = ->(_) { [404, { 'content-type'.freeze => 'application/json'.freeze }, ['{}'.freeze]] }
27
+ app = ->(_) { [404, { Rack::CONTENT_TYPE => 'application/json'.freeze }, ['{}'.freeze]] }
28
28
  builder = Rack::Builder.new
29
29
  yield builder if block_given?
30
30
  builder.use Flipper::Middleware::SetupEnv, flipper, env_key: env_key
@@ -1,5 +1,19 @@
1
1
  module Flipper
2
2
  class Engine < Rails::Engine
3
+ def self.default_strict_value
4
+ value = ENV["FLIPPER_STRICT"]
5
+ if value.in?(["warn", "raise", "noop"])
6
+ value.to_sym
7
+ elsif value
8
+ Typecast.to_boolean(value) ? :raise : false
9
+ elsif Rails.env.production?
10
+ false
11
+ else
12
+ # Warn in development for now. Future versions may default to :raise in development and test
13
+ Rails.env.development? && :warn
14
+ end
15
+ end
16
+
3
17
  paths["config/routes.rb"] = ["lib/flipper/cloud/routes.rb"]
4
18
 
5
19
  config.before_configuration do
@@ -10,7 +24,8 @@ module Flipper
10
24
  instrumenter: ENV.fetch('FLIPPER_INSTRUMENTER', 'ActiveSupport::Notifications').constantize,
11
25
  log: ENV.fetch('FLIPPER_LOG', 'true').casecmp('true').zero?,
12
26
  cloud_path: "_flipper",
13
- strict: default_strict_value
27
+ strict: default_strict_value,
28
+ test_help: Flipper::Typecast.to_boolean(ENV["FLIPPER_TEST_HELP"] || Rails.env.test?),
14
29
  )
15
30
  end
16
31
 
@@ -29,10 +44,6 @@ module Flipper
29
44
  require 'flipper/cloud' if cloud?
30
45
 
31
46
  Flipper.configure do |config|
32
- if app.config.flipper.strict
33
- config.use Flipper::Adapters::Strict, app.config.flipper.strict
34
- end
35
-
36
47
  config.default do
37
48
  if cloud?
38
49
  Flipper::Cloud.new(
@@ -54,6 +65,16 @@ module Flipper
54
65
  end
55
66
  end
56
67
 
68
+ initializer "flipper.strict", after: :load_config_initializers do |app|
69
+ flipper = app.config.flipper
70
+
71
+ if flipper.strict
72
+ Flipper.configure do |config|
73
+ config.use Flipper::Adapters::Strict, flipper.strict
74
+ end
75
+ end
76
+ end
77
+
57
78
  initializer "flipper.memoizer", after: :load_config_initializers do |app|
58
79
  flipper = app.config.flipper
59
80
 
@@ -66,22 +87,16 @@ module Flipper
66
87
  end
67
88
  end
68
89
 
90
+ initializer "flipper.test" do |app|
91
+ require "flipper/test_help" if app.config.flipper.test_help
92
+ end
93
+
69
94
  def cloud?
70
95
  !!ENV["FLIPPER_CLOUD_TOKEN"]
71
96
  end
72
97
 
73
- def default_strict_value
74
- value = ENV["FLIPPER_STRICT"]
75
- if value.in?(["warn", "raise", "noop"])
76
- value.to_sym
77
- elsif value
78
- Typecast.to_boolean(value) ? :raise : false
79
- elsif Rails.env.production?
80
- false
81
- else
82
- # Warn for now. Future versions will default to :raise in development and test
83
- :warn
84
- end
98
+ def self.deprecated_rails_version?
99
+ Gem::Version.new(Rails.version) < Gem::Version.new(Flipper::NEXT_REQUIRED_RAILS_VERSION)
85
100
  end
86
101
  end
87
102
  end