flipper 1.1.1 → 1.2.0

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.
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