flipper 1.1.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![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.
|
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
|
-
| ![@jnunemaker](https://avatars3.githubusercontent.com/u/235?s=64)
|
109
|
-
| ![@bkeepers](https://avatars3.githubusercontent.com/u/173?s=64)
|
110
|
-
| ![@dpep](https://avatars3.githubusercontent.com/u/918804?s=64)
|
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)
|
113
|
-
| ![@lazebny](https://avatars1.githubusercontent.com/u/6276766?s=64)
|
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
|
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
|