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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +25 -1
- data/.github/workflows/examples.yml +7 -1
- data/Changelog.md +1 -638
- data/Gemfile +5 -1
- data/README.md +21 -21
- data/Rakefile +2 -2
- data/exe/flipper +5 -0
- data/flipper.gemspec +6 -2
- data/lib/flipper/adapters/http/client.rb +25 -16
- data/lib/flipper/adapters/strict.rb +11 -8
- data/lib/flipper/cli.rb +240 -0
- data/lib/flipper/cloud/configuration.rb +7 -1
- data/lib/flipper/cloud/middleware.rb +5 -5
- data/lib/flipper/cloud/telemetry/submitter.rb +2 -2
- data/lib/flipper/cloud.rb +1 -1
- data/lib/flipper/engine.rb +32 -17
- data/lib/flipper/instrumentation/log_subscriber.rb +12 -3
- data/lib/flipper/metadata.rb +3 -1
- data/lib/flipper/test_help.rb +36 -0
- data/lib/flipper/version.rb +11 -1
- data/lib/generators/flipper/setup_generator.rb +63 -0
- data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
- data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
- data/lib/generators/flipper/update_generator.rb +35 -0
- data/spec/fixtures/environment.rb +1 -0
- data/spec/flipper/adapter_builder_spec.rb +1 -2
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +92 -75
- data/spec/flipper/adapters/strict_spec.rb +11 -9
- data/spec/flipper/cli_spec.rb +164 -0
- data/spec/flipper/cloud/configuration_spec.rb +9 -2
- data/spec/flipper/cloud/dsl_spec.rb +5 -5
- data/spec/flipper/cloud/middleware_spec.rb +8 -8
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +24 -24
- data/spec/flipper/cloud/telemetry_spec.rb +1 -1
- data/spec/flipper/cloud_spec.rb +4 -4
- data/spec/flipper/engine_spec.rb +76 -11
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +9 -2
- data/spec/flipper_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/support/spec_helpers.rb +10 -4
- data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
- data/test_rails/generators/flipper/update_generator_test.rb +96 -0
- data/test_rails/helper.rb +19 -2
- data/test_rails/system/test_help_test.rb +46 -0
- 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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
[](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.
|
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
|
107
|
-
|
108
|
-
| 
|
109
|
-
| 
|
110
|
-
| 
|
111
|
-
|  | [@alexwheeler](https://github.com/alexwheeler) | api
|
112
|
-
| 
|
113
|
-
| 
|
106
|
+
| pic | @mention | area |
|
107
|
+
| ---------------------------------------------------------------------- | ---------------------------------------------- | ----------- |
|
108
|
+
|  | [@jnunemaker](https://github.com/jnunemaker) | most things |
|
109
|
+
|  | [@bkeepers](https://github.com/bkeepers) | most things |
|
110
|
+
|  | [@dpep](https://github.com/dpep) | tbd |
|
111
|
+
|  | [@alexwheeler](https://github.com/alexwheeler) | api |
|
112
|
+
|  | [@thetimbanks](https://github.com/thetimbanks) | ui |
|
113
|
+
|  | [@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
|
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
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 =
|
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
|
-
'
|
11
|
-
'
|
12
|
-
'
|
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:
|
19
|
-
sinatra:
|
20
|
-
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
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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("
|
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 ||
|
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
|
-
|
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
|
data/lib/flipper/cli.rb
ADDED
@@ -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
|
-
"
|
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
|
-
|
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["
|
45
|
-
headers["
|
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["
|
49
|
-
headers["
|
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
|
82
|
-
client.add_header
|
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, {
|
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
|
data/lib/flipper/engine.rb
CHANGED
@@ -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
|
74
|
-
|
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
|