flipper 1.0.0 → 1.3.6
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/FUNDING.yml +1 -0
- data/.github/workflows/ci.yml +50 -7
- data/.github/workflows/examples.yml +50 -8
- data/CLAUDE.md +74 -0
- data/Changelog.md +1 -584
- data/Gemfile +15 -8
- data/README.md +31 -27
- data/Rakefile +2 -2
- data/benchmark/typecast_ips.rb +8 -0
- data/docs/images/banner.jpg +0 -0
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/cloud/backoff_policy.rb +13 -0
- data/examples/cloud/cloud_setup.rb +16 -0
- data/examples/cloud/forked.rb +7 -2
- data/examples/cloud/threaded.rb +15 -18
- data/examples/expressions.rb +213 -0
- data/examples/strict.rb +18 -0
- data/exe/flipper +5 -0
- data/flipper.gemspec +6 -3
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +10 -0
- data/lib/flipper/adapter_builder.rb +44 -0
- data/lib/flipper/adapters/actor_limit.rb +28 -0
- data/lib/flipper/adapters/cache_base.rb +143 -0
- data/lib/flipper/adapters/dual_write.rb +1 -3
- data/lib/flipper/adapters/failover.rb +0 -4
- data/lib/flipper/adapters/failsafe.rb +0 -4
- data/lib/flipper/adapters/http/client.rb +40 -12
- data/lib/flipper/adapters/http/error.rb +2 -2
- data/lib/flipper/adapters/http.rb +19 -14
- data/lib/flipper/adapters/instrumented.rb +0 -4
- data/lib/flipper/adapters/memoizable.rb +14 -19
- data/lib/flipper/adapters/memory.rb +4 -6
- data/lib/flipper/adapters/operation_logger.rb +18 -92
- data/lib/flipper/adapters/poll.rb +16 -3
- data/lib/flipper/adapters/pstore.rb +17 -11
- data/lib/flipper/adapters/read_only.rb +8 -41
- data/lib/flipper/adapters/strict.rb +45 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
- data/lib/flipper/adapters/sync.rb +0 -4
- data/lib/flipper/adapters/wrapper.rb +54 -0
- data/lib/flipper/cli.rb +263 -0
- data/lib/flipper/cloud/configuration.rb +131 -54
- data/lib/flipper/cloud/middleware.rb +5 -5
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
- data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
- data/lib/flipper/cloud/telemetry/metric.rb +39 -0
- data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
- data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
- data/lib/flipper/cloud/telemetry.rb +191 -0
- data/lib/flipper/cloud.rb +1 -1
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +51 -0
- data/lib/flipper/engine.rb +42 -3
- data/lib/flipper/export.rb +0 -2
- data/lib/flipper/exporters/json/export.rb +1 -1
- data/lib/flipper/exporters/json/v1.rb +1 -1
- data/lib/flipper/expression/builder.rb +73 -0
- data/lib/flipper/expression/constant.rb +25 -0
- data/lib/flipper/expression.rb +71 -0
- data/lib/flipper/expressions/all.rb +9 -0
- data/lib/flipper/expressions/any.rb +9 -0
- data/lib/flipper/expressions/boolean.rb +9 -0
- data/lib/flipper/expressions/comparable.rb +13 -0
- data/lib/flipper/expressions/duration.rb +28 -0
- data/lib/flipper/expressions/equal.rb +9 -0
- data/lib/flipper/expressions/greater_than.rb +9 -0
- data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/less_than.rb +9 -0
- data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/not_equal.rb +9 -0
- data/lib/flipper/expressions/now.rb +9 -0
- data/lib/flipper/expressions/number.rb +9 -0
- data/lib/flipper/expressions/percentage.rb +9 -0
- data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
- data/lib/flipper/expressions/property.rb +9 -0
- data/lib/flipper/expressions/random.rb +9 -0
- data/lib/flipper/expressions/string.rb +9 -0
- data/lib/flipper/expressions/time.rb +9 -0
- data/lib/flipper/feature.rb +63 -1
- data/lib/flipper/gate.rb +2 -1
- data/lib/flipper/gate_values.rb +5 -2
- data/lib/flipper/gates/expression.rb +75 -0
- data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
- data/lib/flipper/instrumentation/statsd.rb +4 -2
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/instrumentation/subscriber.rb +0 -4
- data/lib/flipper/metadata.rb +4 -1
- data/lib/flipper/middleware/memoizer.rb +29 -13
- data/lib/flipper/model/active_record.rb +23 -0
- data/lib/flipper/poller.rb +9 -8
- data/lib/flipper/serializers/gzip.rb +22 -0
- data/lib/flipper/serializers/json.rb +17 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +46 -27
- data/lib/flipper/test/shared_adapter_test.rb +41 -22
- data/lib/flipper/test_help.rb +43 -0
- data/lib/flipper/typecast.rb +37 -9
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +11 -1
- data/lib/flipper.rb +41 -2
- data/lib/generators/flipper/setup_generator.rb +68 -0
- data/lib/generators/flipper/templates/initializer.rb +45 -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/package-lock.json +41 -0
- data/package.json +10 -0
- data/spec/fixtures/environment.rb +1 -0
- data/spec/flipper/adapter_builder_spec.rb +72 -0
- data/spec/flipper/adapter_spec.rb +1 -0
- data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +135 -74
- data/spec/flipper/adapters/memoizable_spec.rb +15 -15
- data/spec/flipper/adapters/poll_spec.rb +41 -0
- data/spec/flipper/adapters/read_only_spec.rb +26 -11
- data/spec/flipper/adapters/strict_spec.rb +64 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
- data/spec/flipper/cli_spec.rb +166 -0
- data/spec/flipper/cloud/configuration_spec.rb +39 -57
- data/spec/flipper/cloud/dsl_spec.rb +6 -6
- data/spec/flipper/cloud/middleware_spec.rb +8 -8
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
- data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
- data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
- data/spec/flipper/cloud/telemetry_spec.rb +208 -0
- data/spec/flipper/cloud_spec.rb +31 -25
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +39 -3
- data/spec/flipper/engine_spec.rb +226 -42
- data/spec/flipper/exporters/json/v1_spec.rb +3 -3
- data/spec/flipper/expression/builder_spec.rb +248 -0
- data/spec/flipper/expression_spec.rb +188 -0
- data/spec/flipper/expressions/all_spec.rb +15 -0
- data/spec/flipper/expressions/any_spec.rb +15 -0
- data/spec/flipper/expressions/boolean_spec.rb +15 -0
- data/spec/flipper/expressions/duration_spec.rb +43 -0
- data/spec/flipper/expressions/equal_spec.rb +24 -0
- data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/greater_than_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_spec.rb +32 -0
- data/spec/flipper/expressions/not_equal_spec.rb +15 -0
- data/spec/flipper/expressions/now_spec.rb +11 -0
- data/spec/flipper/expressions/number_spec.rb +21 -0
- data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
- data/spec/flipper/expressions/percentage_spec.rb +15 -0
- data/spec/flipper/expressions/property_spec.rb +13 -0
- data/spec/flipper/expressions/random_spec.rb +9 -0
- data/spec/flipper/expressions/string_spec.rb +11 -0
- data/spec/flipper/expressions/time_spec.rb +13 -0
- data/spec/flipper/feature_spec.rb +380 -10
- data/spec/flipper/gate_values_spec.rb +2 -2
- data/spec/flipper/gates/expression_spec.rb +108 -0
- data/spec/flipper/identifier_spec.rb +4 -5
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
- data/spec/flipper/middleware/memoizer_spec.rb +79 -10
- data/spec/flipper/model/active_record_spec.rb +72 -0
- data/spec/flipper/serializers/gzip_spec.rb +13 -0
- data/spec/flipper/serializers/json_spec.rb +13 -0
- data/spec/flipper/typecast_spec.rb +43 -7
- data/spec/flipper/types/actor_spec.rb +18 -1
- data/spec/flipper_integration_spec.rb +102 -4
- data/spec/flipper_spec.rb +91 -3
- data/spec/spec_helper.rb +17 -5
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/fail_on_output.rb +8 -0
- data/spec/support/fake_backoff_policy.rb +15 -0
- data/spec/support/spec_helpers.rb +34 -8
- data/test/adapters/actor_limit_test.rb +20 -0
- data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
- data/test_rails/generators/flipper/update_generator_test.rb +96 -0
- data/test_rails/helper.rb +22 -2
- data/test_rails/system/test_help_test.rb +52 -0
- metadata +145 -29
- data/lib/flipper/cloud/instrumenter.rb +0 -48
- data/spec/support/climate_control.rb +0 -7
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
module Flipper
|
|
2
|
+
module Adapters
|
|
3
|
+
# Base class for caching adapters. Inherit from this and then override
|
|
4
|
+
# cache_fetch, cache_read_multi, cache_write, and cache_delete.
|
|
5
|
+
class CacheBase
|
|
6
|
+
include ::Flipper::Adapter
|
|
7
|
+
|
|
8
|
+
# Public: The adapter being cached.
|
|
9
|
+
attr_reader :adapter
|
|
10
|
+
|
|
11
|
+
# Public: The ActiveSupport::Cache::Store to cache with.
|
|
12
|
+
attr_reader :cache
|
|
13
|
+
|
|
14
|
+
# Public: The ttl for all cached data.
|
|
15
|
+
attr_reader :ttl
|
|
16
|
+
|
|
17
|
+
# Public: The cache key where the set of known features is cached.
|
|
18
|
+
attr_reader :features_cache_key
|
|
19
|
+
|
|
20
|
+
# Public: Alias expires_in to ttl for compatibility.
|
|
21
|
+
alias_method :expires_in, :ttl
|
|
22
|
+
|
|
23
|
+
def initialize(adapter, cache, ttl = 300, prefix: nil)
|
|
24
|
+
@adapter = adapter
|
|
25
|
+
@cache = cache
|
|
26
|
+
@ttl = ttl
|
|
27
|
+
|
|
28
|
+
@cache_version = 'v1'.freeze
|
|
29
|
+
@namespace = "flipper/#{@cache_version}"
|
|
30
|
+
@namespace = @namespace.prepend(prefix) if prefix
|
|
31
|
+
@features_cache_key = "#{@namespace}/features"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Public: Expire the cache for the set of known feature names.
|
|
35
|
+
def expire_features_cache
|
|
36
|
+
cache_delete @features_cache_key
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Public: Expire the cache for a given feature.
|
|
40
|
+
def expire_feature_cache(key)
|
|
41
|
+
cache_delete feature_cache_key(key)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Public
|
|
45
|
+
def features
|
|
46
|
+
read_feature_keys
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Public
|
|
50
|
+
def add(feature)
|
|
51
|
+
result = @adapter.add(feature)
|
|
52
|
+
expire_features_cache
|
|
53
|
+
result
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Public
|
|
57
|
+
def remove(feature)
|
|
58
|
+
result = @adapter.remove(feature)
|
|
59
|
+
expire_features_cache
|
|
60
|
+
expire_feature_cache(feature.key)
|
|
61
|
+
result
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Public
|
|
65
|
+
def clear(feature)
|
|
66
|
+
result = @adapter.clear(feature)
|
|
67
|
+
expire_feature_cache(feature.key)
|
|
68
|
+
result
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Public
|
|
72
|
+
def get(feature)
|
|
73
|
+
read_feature(feature)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Public
|
|
77
|
+
def get_multi(features)
|
|
78
|
+
read_many_features(features)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Public
|
|
82
|
+
def get_all
|
|
83
|
+
features = read_feature_keys.map { |key| Flipper::Feature.new(key, self) }
|
|
84
|
+
read_many_features(features)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Public
|
|
88
|
+
def enable(feature, gate, thing)
|
|
89
|
+
result = @adapter.enable(feature, gate, thing)
|
|
90
|
+
expire_feature_cache(feature.key)
|
|
91
|
+
result
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Public
|
|
95
|
+
def disable(feature, gate, thing)
|
|
96
|
+
result = @adapter.disable(feature, gate, thing)
|
|
97
|
+
expire_feature_cache(feature.key)
|
|
98
|
+
result
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Public: Generate the cache key for a given feature.
|
|
102
|
+
#
|
|
103
|
+
# key - The String or Symbol feature key.
|
|
104
|
+
def feature_cache_key(key)
|
|
105
|
+
"#{@namespace}/feature/#{key}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# Private: Returns the Set of known feature keys.
|
|
111
|
+
def read_feature_keys
|
|
112
|
+
cache_fetch(@features_cache_key) { @adapter.features }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Private: Read through caching for a single feature.
|
|
116
|
+
def read_feature(feature)
|
|
117
|
+
cache_fetch(feature_cache_key(feature.key)) { @adapter.get(feature) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Private: Given an array of features, attempts to read through cache in
|
|
121
|
+
# as few network calls as possible.
|
|
122
|
+
def read_many_features(features)
|
|
123
|
+
keys = features.map { |feature| feature_cache_key(feature.key) }
|
|
124
|
+
cache_result = cache_read_multi(keys)
|
|
125
|
+
uncached_features = features.reject { |feature| cache_result[feature_cache_key(feature)] }
|
|
126
|
+
|
|
127
|
+
if uncached_features.any?
|
|
128
|
+
response = @adapter.get_multi(uncached_features)
|
|
129
|
+
response.each do |key, value|
|
|
130
|
+
cache_write feature_cache_key(key), value
|
|
131
|
+
cache_result[feature_cache_key(key)] = value
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
result = {}
|
|
136
|
+
features.each do |feature|
|
|
137
|
+
result[feature.key] = cache_result[feature_cache_key(feature.key)]
|
|
138
|
+
end
|
|
139
|
+
result
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -3,8 +3,7 @@ module Flipper
|
|
|
3
3
|
class DualWrite
|
|
4
4
|
include ::Flipper::Adapter
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
attr_reader :name, :local, :remote
|
|
6
|
+
attr_reader :local, :remote
|
|
8
7
|
|
|
9
8
|
# Public: Build a new sync instance.
|
|
10
9
|
#
|
|
@@ -12,7 +11,6 @@ module Flipper
|
|
|
12
11
|
# remote - The remote flipper adapter that writes should go to first (in
|
|
13
12
|
# addition to the local adapter).
|
|
14
13
|
def initialize(local, remote, options = {})
|
|
15
|
-
@name = :dual_write
|
|
16
14
|
@local = local
|
|
17
15
|
@remote = remote
|
|
18
16
|
end
|
|
@@ -3,9 +3,6 @@ module Flipper
|
|
|
3
3
|
class Failover
|
|
4
4
|
include ::Flipper::Adapter
|
|
5
5
|
|
|
6
|
-
# Public: The name of the adapter.
|
|
7
|
-
attr_reader :name
|
|
8
|
-
|
|
9
6
|
# Public: Build a new failover instance.
|
|
10
7
|
#
|
|
11
8
|
# primary - The primary flipper adapter.
|
|
@@ -17,7 +14,6 @@ module Flipper
|
|
|
17
14
|
# :errors - Array of exception types for which to failover
|
|
18
15
|
|
|
19
16
|
def initialize(primary, secondary, options = {})
|
|
20
|
-
@name = :failover
|
|
21
17
|
@primary = primary
|
|
22
18
|
@secondary = secondary
|
|
23
19
|
|
|
@@ -3,9 +3,6 @@ module Flipper
|
|
|
3
3
|
class Failsafe
|
|
4
4
|
include ::Flipper::Adapter
|
|
5
5
|
|
|
6
|
-
# Public: The name of the adapter.
|
|
7
|
-
attr_reader :name
|
|
8
|
-
|
|
9
6
|
# Public: Build a new Failsafe instance.
|
|
10
7
|
#
|
|
11
8
|
# adapter - Flipper adapter to guard.
|
|
@@ -15,7 +12,6 @@ module Flipper
|
|
|
15
12
|
def initialize(adapter, options = {})
|
|
16
13
|
@adapter = adapter
|
|
17
14
|
@errors = options.fetch(:errors, [StandardError])
|
|
18
|
-
@name = :failsafe
|
|
19
15
|
end
|
|
20
16
|
|
|
21
17
|
def features
|
|
@@ -7,20 +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
|
+
CLIENT_FRAMEWORKS = {
|
|
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) },
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
attr_reader :uri, :headers
|
|
18
26
|
attr_reader :basic_auth_username, :basic_auth_password
|
|
19
|
-
attr_reader :read_timeout, :open_timeout, :write_timeout
|
|
27
|
+
attr_reader :read_timeout, :open_timeout, :write_timeout
|
|
28
|
+
attr_reader :max_retries, :debug_output
|
|
20
29
|
|
|
21
30
|
def initialize(options = {})
|
|
22
31
|
@uri = URI(options.fetch(:url))
|
|
23
|
-
@headers = DEFAULT_HEADERS.merge(options[:headers] || {})
|
|
24
32
|
@basic_auth_username = options[:basic_auth_username]
|
|
25
33
|
@basic_auth_password = options[:basic_auth_password]
|
|
26
34
|
@read_timeout = options[:read_timeout]
|
|
@@ -28,6 +36,17 @@ module Flipper
|
|
|
28
36
|
@write_timeout = options[:write_timeout]
|
|
29
37
|
@max_retries = options.key?(:max_retries) ? options[:max_retries] : 0
|
|
30
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
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def add_header(key, value)
|
|
48
|
+
key = key.to_s.downcase.gsub('_'.freeze, '-'.freeze).freeze
|
|
49
|
+
@headers[key] = value
|
|
31
50
|
end
|
|
32
51
|
|
|
33
52
|
def get(path)
|
|
@@ -77,18 +96,23 @@ module Flipper
|
|
|
77
96
|
|
|
78
97
|
def build_request(http_method, uri, headers, options)
|
|
79
98
|
request_headers = {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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,
|
|
87
106
|
}.merge(headers)
|
|
88
107
|
|
|
89
108
|
body = options[:body]
|
|
90
109
|
request = http_method.new(uri.request_uri)
|
|
91
110
|
request.initialize_http_header(request_headers)
|
|
111
|
+
|
|
112
|
+
client_frameworks.each do |framework, version|
|
|
113
|
+
request.add_field("client-framework", [framework, version].join("="))
|
|
114
|
+
end
|
|
115
|
+
|
|
92
116
|
request.body = body if body
|
|
93
117
|
|
|
94
118
|
if @basic_auth_username && @basic_auth_password
|
|
@@ -97,6 +121,10 @@ module Flipper
|
|
|
97
121
|
|
|
98
122
|
request
|
|
99
123
|
end
|
|
124
|
+
|
|
125
|
+
def client_frameworks
|
|
126
|
+
CLIENT_FRAMEWORKS.transform_values { |detect| detect.call rescue nil }.compact
|
|
127
|
+
end
|
|
100
128
|
end
|
|
101
129
|
end
|
|
102
130
|
end
|
|
@@ -11,7 +11,7 @@ module Flipper
|
|
|
11
11
|
message = "Failed with status: #{response.code}"
|
|
12
12
|
|
|
13
13
|
begin
|
|
14
|
-
data =
|
|
14
|
+
data = Typecast.from_json(response.body)
|
|
15
15
|
|
|
16
16
|
if error_message = data["message"]
|
|
17
17
|
message << "\n\n#{data["message"]}"
|
|
@@ -20,7 +20,7 @@ module Flipper
|
|
|
20
20
|
if more_info = data["more_info"]
|
|
21
21
|
message << "\n#{data["more_info"]}"
|
|
22
22
|
end
|
|
23
|
-
rescue
|
|
23
|
+
rescue
|
|
24
24
|
# welp we tried
|
|
25
25
|
end
|
|
26
26
|
|
|
@@ -10,7 +10,7 @@ module Flipper
|
|
|
10
10
|
class Http
|
|
11
11
|
include Flipper::Adapter
|
|
12
12
|
|
|
13
|
-
attr_reader :
|
|
13
|
+
attr_reader :client
|
|
14
14
|
|
|
15
15
|
def initialize(options = {})
|
|
16
16
|
@client = Client.new(url: options.fetch(:url),
|
|
@@ -22,13 +22,12 @@ module Flipper
|
|
|
22
22
|
write_timeout: options[:write_timeout],
|
|
23
23
|
max_retries: options[:max_retries],
|
|
24
24
|
debug_output: options[:debug_output])
|
|
25
|
-
@name = :http
|
|
26
25
|
end
|
|
27
26
|
|
|
28
27
|
def get(feature)
|
|
29
28
|
response = @client.get("/features/#{feature.key}")
|
|
30
29
|
if response.is_a?(Net::HTTPOK)
|
|
31
|
-
parsed_response =
|
|
30
|
+
parsed_response = Typecast.from_json(response.body)
|
|
32
31
|
result_for_feature(feature, parsed_response.fetch('gates'))
|
|
33
32
|
elsif response.is_a?(Net::HTTPNotFound)
|
|
34
33
|
default_config
|
|
@@ -42,7 +41,7 @@ module Flipper
|
|
|
42
41
|
response = @client.get("/features?keys=#{csv_keys}&exclude_gate_names=true")
|
|
43
42
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
|
44
43
|
|
|
45
|
-
parsed_response =
|
|
44
|
+
parsed_response = Typecast.from_json(response.body)
|
|
46
45
|
parsed_features = parsed_response.fetch('features')
|
|
47
46
|
gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
|
|
48
47
|
hash[parsed_feature['key']] = parsed_feature['gates']
|
|
@@ -60,15 +59,15 @@ module Flipper
|
|
|
60
59
|
response = @client.get("/features?exclude_gate_names=true")
|
|
61
60
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
|
62
61
|
|
|
63
|
-
parsed_response =
|
|
64
|
-
parsed_features = parsed_response
|
|
62
|
+
parsed_response = response.body.empty? ? {} : Typecast.from_json(response.body)
|
|
63
|
+
parsed_features = parsed_response['features'] || []
|
|
65
64
|
gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
|
|
66
65
|
hash[parsed_feature['key']] = parsed_feature['gates']
|
|
67
66
|
hash
|
|
68
67
|
end
|
|
69
68
|
|
|
70
69
|
result = {}
|
|
71
|
-
gates_by_key.
|
|
70
|
+
gates_by_key.each_key do |key|
|
|
72
71
|
feature = Feature.new(key, self)
|
|
73
72
|
result[feature.key] = result_for_feature(feature, gates_by_key[feature.key])
|
|
74
73
|
end
|
|
@@ -79,7 +78,7 @@ module Flipper
|
|
|
79
78
|
response = @client.get('/features?exclude_gate_names=true')
|
|
80
79
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
|
81
80
|
|
|
82
|
-
parsed_response =
|
|
81
|
+
parsed_response = Typecast.from_json(response.body)
|
|
83
82
|
parsed_response['features'].map { |feature| feature['key'] }.to_set
|
|
84
83
|
end
|
|
85
84
|
|
|
@@ -97,7 +96,7 @@ module Flipper
|
|
|
97
96
|
end
|
|
98
97
|
|
|
99
98
|
def enable(feature, gate, thing)
|
|
100
|
-
body = request_body_for_gate(gate, thing.value
|
|
99
|
+
body = request_body_for_gate(gate, thing.value)
|
|
101
100
|
query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
|
|
102
101
|
response = @client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
|
|
103
102
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
|
@@ -105,7 +104,7 @@ module Flipper
|
|
|
105
104
|
end
|
|
106
105
|
|
|
107
106
|
def disable(feature, gate, thing)
|
|
108
|
-
body = request_body_for_gate(gate, thing.value
|
|
107
|
+
body = request_body_for_gate(gate, thing.value)
|
|
109
108
|
query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
|
|
110
109
|
response = case gate.key
|
|
111
110
|
when :percentage_of_actors, :percentage_of_time
|
|
@@ -138,11 +137,13 @@ module Flipper
|
|
|
138
137
|
when :boolean
|
|
139
138
|
{}
|
|
140
139
|
when :groups
|
|
141
|
-
{ name: value }
|
|
140
|
+
{ name: value.to_s }
|
|
142
141
|
when :actors
|
|
143
|
-
{ flipper_id: value }
|
|
142
|
+
{ flipper_id: value.to_s }
|
|
144
143
|
when :percentage_of_actors, :percentage_of_time
|
|
145
|
-
{ percentage: value }
|
|
144
|
+
{ percentage: value.to_s }
|
|
145
|
+
when :expression
|
|
146
|
+
value
|
|
146
147
|
else
|
|
147
148
|
raise "#{gate.key} is not a valid flipper gate key"
|
|
148
149
|
end
|
|
@@ -166,13 +167,17 @@ module Flipper
|
|
|
166
167
|
case gate.data_type
|
|
167
168
|
when :boolean, :integer
|
|
168
169
|
value ? value.to_s : value
|
|
170
|
+
when :json
|
|
171
|
+
value
|
|
169
172
|
when :set
|
|
170
173
|
value ? value.to_set : Set.new
|
|
171
174
|
else
|
|
172
|
-
unsupported_data_type
|
|
175
|
+
unsupported_data_type gate.data_type
|
|
173
176
|
end
|
|
174
177
|
end
|
|
175
178
|
|
|
179
|
+
private
|
|
180
|
+
|
|
176
181
|
def unsupported_data_type(data_type)
|
|
177
182
|
raise "#{data_type} is not supported by this adapter"
|
|
178
183
|
end
|
|
@@ -13,9 +13,6 @@ module Flipper
|
|
|
13
13
|
# Private: What is used to instrument all the things.
|
|
14
14
|
attr_reader :instrumenter
|
|
15
15
|
|
|
16
|
-
# Public: The name of the adapter.
|
|
17
|
-
attr_reader :name
|
|
18
|
-
|
|
19
16
|
# Internal: Initializes a new adapter instance.
|
|
20
17
|
#
|
|
21
18
|
# adapter - Vanilla adapter instance to wrap.
|
|
@@ -25,7 +22,6 @@ module Flipper
|
|
|
25
22
|
#
|
|
26
23
|
def initialize(adapter, options = {})
|
|
27
24
|
@adapter = adapter
|
|
28
|
-
@name = :instrumented
|
|
29
25
|
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
|
30
26
|
end
|
|
31
27
|
|
|
@@ -8,35 +8,25 @@ module Flipper
|
|
|
8
8
|
class Memoizable
|
|
9
9
|
include ::Flipper::Adapter
|
|
10
10
|
|
|
11
|
-
FeaturesKey = :flipper_features
|
|
12
|
-
GetAllKey = :all_memoized
|
|
13
|
-
|
|
14
11
|
# Internal
|
|
15
12
|
attr_reader :cache
|
|
16
13
|
|
|
17
|
-
# Public: The name of the adapter.
|
|
18
|
-
attr_reader :name
|
|
19
|
-
|
|
20
14
|
# Internal: The adapter this adapter is wrapping.
|
|
21
15
|
attr_reader :adapter
|
|
22
16
|
|
|
23
|
-
# Private
|
|
24
|
-
def self.key_for(key)
|
|
25
|
-
"feature/#{key}"
|
|
26
|
-
end
|
|
27
|
-
|
|
28
17
|
# Public
|
|
29
18
|
def initialize(adapter, cache = nil)
|
|
30
19
|
@adapter = adapter
|
|
31
|
-
@name = :memoizable
|
|
32
20
|
@cache = cache || {}
|
|
33
21
|
@memoize = false
|
|
22
|
+
@features_key = :flipper_features
|
|
23
|
+
@get_all_key = :all_memoized
|
|
34
24
|
end
|
|
35
25
|
|
|
36
26
|
# Public
|
|
37
27
|
def features
|
|
38
28
|
if memoizing?
|
|
39
|
-
cache.fetch(
|
|
29
|
+
cache.fetch(@features_key) { cache[@features_key] = @adapter.features }
|
|
40
30
|
else
|
|
41
31
|
@adapter.features
|
|
42
32
|
end
|
|
@@ -94,9 +84,9 @@ module Flipper
|
|
|
94
84
|
def get_all
|
|
95
85
|
if memoizing?
|
|
96
86
|
response = nil
|
|
97
|
-
if cache[
|
|
87
|
+
if cache[@get_all_key]
|
|
98
88
|
response = {}
|
|
99
|
-
cache[
|
|
89
|
+
cache[@features_key].each do |key|
|
|
100
90
|
response[key] = cache[key_for(key)]
|
|
101
91
|
end
|
|
102
92
|
else
|
|
@@ -104,8 +94,8 @@ module Flipper
|
|
|
104
94
|
response.each do |key, value|
|
|
105
95
|
cache[key_for(key)] = value
|
|
106
96
|
end
|
|
107
|
-
cache[
|
|
108
|
-
cache[
|
|
97
|
+
cache[@features_key] = response.keys.to_set
|
|
98
|
+
cache[@get_all_key] = true
|
|
109
99
|
end
|
|
110
100
|
|
|
111
101
|
# Ensures that looking up other features that do not exist doesn't
|
|
@@ -127,6 +117,11 @@ module Flipper
|
|
|
127
117
|
@adapter.disable(feature, gate, thing).tap { expire_feature(feature) }
|
|
128
118
|
end
|
|
129
119
|
|
|
120
|
+
# Public
|
|
121
|
+
def read_only?
|
|
122
|
+
@adapter.read_only?
|
|
123
|
+
end
|
|
124
|
+
|
|
130
125
|
def import(source)
|
|
131
126
|
@adapter.import(source).tap { cache.clear if memoizing? }
|
|
132
127
|
end
|
|
@@ -161,7 +156,7 @@ module Flipper
|
|
|
161
156
|
private
|
|
162
157
|
|
|
163
158
|
def key_for(key)
|
|
164
|
-
|
|
159
|
+
"feature/#{key}"
|
|
165
160
|
end
|
|
166
161
|
|
|
167
162
|
def expire_feature(feature)
|
|
@@ -169,7 +164,7 @@ module Flipper
|
|
|
169
164
|
end
|
|
170
165
|
|
|
171
166
|
def expire_features_set
|
|
172
|
-
cache.delete(
|
|
167
|
+
cache.delete(@features_key) if memoizing?
|
|
173
168
|
end
|
|
174
169
|
end
|
|
175
170
|
end
|
|
@@ -8,15 +8,9 @@ module Flipper
|
|
|
8
8
|
class Memory
|
|
9
9
|
include ::Flipper::Adapter
|
|
10
10
|
|
|
11
|
-
FeaturesKey = :features
|
|
12
|
-
|
|
13
|
-
# Public: The name of the adapter.
|
|
14
|
-
attr_reader :name
|
|
15
|
-
|
|
16
11
|
# Public
|
|
17
12
|
def initialize(source = nil, threadsafe: true)
|
|
18
13
|
@source = Typecast.features_hash(source)
|
|
19
|
-
@name = :memory
|
|
20
14
|
@lock = Mutex.new if threadsafe
|
|
21
15
|
reset
|
|
22
16
|
end
|
|
@@ -77,6 +71,8 @@ module Flipper
|
|
|
77
71
|
@source[feature.key][gate.key] = thing.value.to_s
|
|
78
72
|
when :set
|
|
79
73
|
@source[feature.key][gate.key] << thing.value.to_s
|
|
74
|
+
when :json
|
|
75
|
+
@source[feature.key][gate.key] = thing.value
|
|
80
76
|
else
|
|
81
77
|
raise "#{gate} is not supported by this adapter yet"
|
|
82
78
|
end
|
|
@@ -97,6 +93,8 @@ module Flipper
|
|
|
97
93
|
@source[feature.key][gate.key] = thing.value.to_s
|
|
98
94
|
when :set
|
|
99
95
|
@source[feature.key][gate.key].delete thing.value.to_s
|
|
96
|
+
when :json
|
|
97
|
+
@source[feature.key].delete(gate.key)
|
|
100
98
|
else
|
|
101
99
|
raise "#{gate} is not supported by this adapter yet"
|
|
102
100
|
end
|