flipper 0.26.2 → 0.27.1
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/Changelog.md +8 -0
- data/Gemfile +2 -3
- data/Rakefile +3 -3
- data/examples/api/basic.ru +3 -4
- data/examples/api/custom_memoized.ru +3 -4
- data/examples/api/memoized.ru +3 -4
- data/lib/flipper/adapter.rb +23 -7
- data/lib/flipper/adapters/http.rb +11 -3
- data/lib/flipper/adapters/instrumented.rb +25 -2
- data/lib/flipper/adapters/memoizable.rb +19 -2
- data/lib/flipper/adapters/memory.rb +8 -4
- data/lib/flipper/adapters/operation_logger.rb +16 -3
- data/lib/flipper/dsl.rb +1 -5
- data/lib/flipper/export.rb +26 -0
- data/lib/flipper/exporter.rb +17 -0
- data/lib/flipper/exporters/json/export.rb +32 -0
- data/lib/flipper/exporters/json/v1.rb +33 -0
- data/lib/flipper/instrumentation/subscriber.rb +8 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +23 -0
- data/lib/flipper/test/shared_adapter_test.rb +24 -0
- data/lib/flipper/typecast.rb +17 -0
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +2 -1
- data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/adapter_spec.rb +29 -2
- data/spec/flipper/adapters/http_spec.rb +25 -3
- data/spec/flipper/adapters/instrumented_spec.rb +28 -10
- data/spec/flipper/adapters/memoizable_spec.rb +30 -10
- data/spec/flipper/adapters/operation_logger_spec.rb +29 -10
- data/spec/flipper/dsl_spec.rb +20 -3
- data/spec/flipper/export_spec.rb +13 -0
- data/spec/flipper/exporter_spec.rb +16 -0
- data/spec/flipper/exporters/json/export_spec.rb +60 -0
- data/spec/flipper/exporters/json/v1_spec.rb +33 -0
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -0
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -0
- data/spec/flipper/typecast_spec.rb +79 -0
- data/spec/flipper_spec.rb +7 -1
- data/spec/support/skippable.rb +18 -0
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ba1d158db684bce01872e3f32c81f522d32086a78af94a78a6266422079a3473
|
4
|
+
data.tar.gz: 9a7ea456415d5fd73d5adcb66bb77c4701a2a6e6b3ab94d5c89af52202907e7c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f708342f07ebe1bfbf3c9d0e0f1db184ccd686bdd995752b6459f96ab119fe589573e0077d156afad1ac8f73b45140a54e301ff23ef423e92b3c109e89285992
|
7
|
+
data.tar.gz: e9ca8640f9c9b655ae464651adda6cb6998355b967b58119fc42c2a87d3c3f848538dd05ff36585e75527384aca05ab6be1996558679a0090ef24bb8a6a7a682
|
data/Changelog.md
CHANGED
@@ -2,6 +2,14 @@
|
|
2
2
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
4
4
|
|
5
|
+
## 0.27.1
|
6
|
+
|
7
|
+
* Quick fix for missing require of "flipper/version" that was causing issues with some flipper-ui people.
|
8
|
+
|
9
|
+
## 0.27.0
|
10
|
+
|
11
|
+
* Easy Import/Export (https://github.com/jnunemaker/flipper/pull/709). This has some breaking changes but only if you are using flipper internals. If you are just using Flipper.* methods, you'll be fine.
|
12
|
+
|
5
13
|
## 0.26.2
|
6
14
|
|
7
15
|
* Improve Active Record Adapter get/get_multi/get_all performance by 5-10x when dealing with thousands of gate values (https://github.com/jnunemaker/flipper/pull/707).
|
data/Gemfile
CHANGED
@@ -8,13 +8,12 @@ end
|
|
8
8
|
|
9
9
|
gem 'debug'
|
10
10
|
gem 'rake', '~> 12.3.3'
|
11
|
-
gem 'shotgun', '~> 0.9'
|
12
11
|
gem 'statsd-ruby', '~> 1.2.1'
|
13
12
|
gem 'rspec', '~> 3.0'
|
14
|
-
gem 'rack-test'
|
13
|
+
gem 'rack-test'
|
15
14
|
gem 'sqlite3', "~> #{ENV['SQLITE3_VERSION'] || '1.4.1'}"
|
16
15
|
gem 'rails', "~> #{ENV['RAILS_VERSION'] || '7.0.0'}"
|
17
|
-
gem 'minitest', '~> 5.
|
16
|
+
gem 'minitest', '~> 5.18'
|
18
17
|
gem 'minitest-documentation'
|
19
18
|
gem 'webmock', '~> 3.0'
|
20
19
|
gem 'ice_age'
|
data/Rakefile
CHANGED
@@ -17,9 +17,9 @@ end
|
|
17
17
|
|
18
18
|
desc 'Tags version, pushes to remote, and pushes gem'
|
19
19
|
task release: :build do
|
20
|
-
sh 'git', 'tag', "v#{Flipper::VERSION}"
|
21
|
-
sh 'git push origin main'
|
22
|
-
sh "git push origin v#{Flipper::VERSION}"
|
20
|
+
# sh 'git', 'tag', "v#{Flipper::VERSION}"
|
21
|
+
# sh 'git push origin main'
|
22
|
+
# sh "git push origin v#{Flipper::VERSION}"
|
23
23
|
puts "\nWhat OTP code should be used?"
|
24
24
|
otp_code = STDIN.gets.chomp
|
25
25
|
sh "ls pkg/*.gem | xargs -n 1 gem push --otp #{otp_code}"
|
data/examples/api/basic.ru
CHANGED
@@ -1,19 +1,18 @@
|
|
1
1
|
#
|
2
2
|
# Usage:
|
3
|
-
# # if you want it to not reload and be really fast
|
4
3
|
# bin/rackup examples/api/basic.ru -p 9999
|
5
4
|
#
|
6
|
-
# # if you want reloading
|
7
|
-
# bin/shotgun examples/api/basic.ru -p 9999
|
8
|
-
#
|
9
5
|
# http://localhost:9999/
|
10
6
|
#
|
11
7
|
|
12
8
|
require 'bundler/setup'
|
9
|
+
require 'rack/reloader'
|
13
10
|
require "flipper/api"
|
14
11
|
require "flipper/adapters/pstore"
|
15
12
|
|
16
13
|
# You can uncomment this to get some default data:
|
17
14
|
# Flipper.enable :logging
|
18
15
|
|
16
|
+
use Rack::Reloader
|
17
|
+
|
19
18
|
run Flipper::Api.app
|
@@ -1,15 +1,12 @@
|
|
1
1
|
#
|
2
2
|
# Usage:
|
3
|
-
# # if you want it to not reload and be really fast
|
4
3
|
# bin/rackup examples/api/custom_memoized.ru -p 9999
|
5
4
|
#
|
6
|
-
# # if you want reloading
|
7
|
-
# bin/shotgun examples/api/custom_memoized.ru -p 9999
|
8
|
-
#
|
9
5
|
# http://localhost:9999/
|
10
6
|
#
|
11
7
|
|
12
8
|
require 'bundler/setup'
|
9
|
+
require 'rack/reloader'
|
13
10
|
require "active_support/notifications"
|
14
11
|
require "flipper/api"
|
15
12
|
require "flipper/adapters/pstore"
|
@@ -31,6 +28,8 @@ ActiveSupport::Notifications.subscribe(/.*/, ->(*args) {
|
|
31
28
|
# You can uncomment this to get some default data:
|
32
29
|
# flipper[:logging].enable_percentage_of_time 5
|
33
30
|
|
31
|
+
use Rack::Reloader
|
32
|
+
|
34
33
|
run Flipper::Api.app(flipper) { |builder|
|
35
34
|
builder.use Flipper::Middleware::SetupEnv, flipper
|
36
35
|
builder.use Flipper::Middleware::Memoizer, preload: true
|
data/examples/api/memoized.ru
CHANGED
@@ -1,15 +1,12 @@
|
|
1
1
|
#
|
2
2
|
# Usage:
|
3
|
-
# # if you want it to not reload and be really fast
|
4
3
|
# bin/rackup examples/api/memoized.ru -p 9999
|
5
4
|
#
|
6
|
-
# # if you want reloading
|
7
|
-
# bin/shotgun examples/api/memoized.ru -p 9999
|
8
|
-
#
|
9
5
|
# http://localhost:9999/
|
10
6
|
#
|
11
7
|
|
12
8
|
require 'bundler/setup'
|
9
|
+
require 'rack/reloader'
|
13
10
|
require "active_support/notifications"
|
14
11
|
require "flipper/api"
|
15
12
|
require "flipper/adapters/pstore"
|
@@ -38,6 +35,8 @@ Flipper.register(:admins) { |actor|
|
|
38
35
|
# You can uncomment this to get some default data:
|
39
36
|
# Flipper.enable :logging
|
40
37
|
|
38
|
+
use Rack::Reloader
|
39
|
+
|
41
40
|
run Flipper::Api.app { |builder|
|
42
41
|
builder.use Flipper::Middleware::Memoizer, preload: true
|
43
42
|
}
|
data/lib/flipper/adapter.rb
CHANGED
@@ -1,7 +1,3 @@
|
|
1
|
-
require "set"
|
2
|
-
require "flipper/feature"
|
3
|
-
require "flipper/adapters/sync/synchronizer"
|
4
|
-
|
5
1
|
module Flipper
|
6
2
|
# Adding a module include so we have some hooks for stuff down the road
|
7
3
|
module Adapter
|
@@ -20,6 +16,11 @@ module Flipper
|
|
20
16
|
percentage_of_time: nil,
|
21
17
|
}
|
22
18
|
end
|
19
|
+
|
20
|
+
def from(source)
|
21
|
+
return source if source.is_a?(Flipper::Adapter)
|
22
|
+
source.adapter
|
23
|
+
end
|
23
24
|
end
|
24
25
|
|
25
26
|
# Public: Get all features and gate values in one call. Defaults to one call
|
@@ -43,9 +44,19 @@ module Flipper
|
|
43
44
|
|
44
45
|
# Public: Ensure that adapter is in sync with source adapter provided.
|
45
46
|
#
|
46
|
-
#
|
47
|
-
|
48
|
-
|
47
|
+
# source - The source dsl, adapter or export to import.
|
48
|
+
#
|
49
|
+
# Returns true if successful.
|
50
|
+
def import(source)
|
51
|
+
Adapters::Sync::Synchronizer.new(self, self.class.from(source), raise: true).call
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
# Public: Exports the adapter in a given format for a given format version.
|
56
|
+
#
|
57
|
+
# Returns a Flipper::Export instance.
|
58
|
+
def export(format: :json, version: 1)
|
59
|
+
Flipper::Exporter.build(format: format, version: version).call(self)
|
49
60
|
end
|
50
61
|
|
51
62
|
# Public: Default config for a feature's gate values.
|
@@ -54,3 +65,8 @@ module Flipper
|
|
54
65
|
end
|
55
66
|
end
|
56
67
|
end
|
68
|
+
|
69
|
+
require "set"
|
70
|
+
require "flipper/exporter"
|
71
|
+
require "flipper/feature"
|
72
|
+
require "flipper/adapters/sync/synchronizer"
|
@@ -39,7 +39,7 @@ module Flipper
|
|
39
39
|
|
40
40
|
def get_multi(features)
|
41
41
|
csv_keys = features.map(&:key).join(',')
|
42
|
-
response = @client.get("/features?keys=#{csv_keys}")
|
42
|
+
response = @client.get("/features?keys=#{csv_keys}&exclude_gate_names=true")
|
43
43
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
44
44
|
|
45
45
|
parsed_response = JSON.parse(response.body)
|
@@ -57,7 +57,7 @@ module Flipper
|
|
57
57
|
end
|
58
58
|
|
59
59
|
def get_all
|
60
|
-
response = @client.get("/features")
|
60
|
+
response = @client.get("/features?exclude_gate_names=true")
|
61
61
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
62
62
|
|
63
63
|
parsed_response = JSON.parse(response.body)
|
@@ -76,7 +76,7 @@ module Flipper
|
|
76
76
|
end
|
77
77
|
|
78
78
|
def features
|
79
|
-
response = @client.get('/features')
|
79
|
+
response = @client.get('/features?exclude_gate_names=true')
|
80
80
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
81
81
|
|
82
82
|
parsed_response = JSON.parse(response.body)
|
@@ -123,6 +123,14 @@ module Flipper
|
|
123
123
|
true
|
124
124
|
end
|
125
125
|
|
126
|
+
def import(source)
|
127
|
+
adapter = self.class.from(source)
|
128
|
+
export = adapter.export(format: :json, version: 1)
|
129
|
+
response = @client.post("/import", export.contents)
|
130
|
+
raise Error, response unless response.is_a?(Net::HTTPNoContent)
|
131
|
+
true
|
132
|
+
end
|
133
|
+
|
126
134
|
private
|
127
135
|
|
128
136
|
def request_body_for_gate(gate, value)
|
@@ -4,7 +4,7 @@ module Flipper
|
|
4
4
|
module Adapters
|
5
5
|
# Internal: Adapter that wraps another adapter and instruments all adapter
|
6
6
|
# operations.
|
7
|
-
class Instrumented
|
7
|
+
class Instrumented
|
8
8
|
include ::Flipper::Adapter
|
9
9
|
|
10
10
|
# Private: The name of instrumentation events.
|
@@ -24,7 +24,6 @@ module Flipper
|
|
24
24
|
# :instrumenter - What to use to instrument all the things.
|
25
25
|
#
|
26
26
|
def initialize(adapter, options = {})
|
27
|
-
super(adapter)
|
28
27
|
@adapter = adapter
|
29
28
|
@name = :instrumented
|
30
29
|
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
@@ -146,6 +145,30 @@ module Flipper
|
|
146
145
|
payload[:result] = @adapter.disable(feature, gate, thing)
|
147
146
|
end
|
148
147
|
end
|
148
|
+
|
149
|
+
def import(source)
|
150
|
+
default_payload = {
|
151
|
+
operation: :import,
|
152
|
+
adapter_name: @adapter.name,
|
153
|
+
}
|
154
|
+
|
155
|
+
@instrumenter.instrument(InstrumentationName, default_payload) do |payload|
|
156
|
+
payload[:result] = @adapter.import(source)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def export(format: :json, version: 1)
|
161
|
+
default_payload = {
|
162
|
+
operation: :export,
|
163
|
+
adapter_name: @adapter.name,
|
164
|
+
format: format,
|
165
|
+
version: version,
|
166
|
+
}
|
167
|
+
|
168
|
+
@instrumenter.instrument(InstrumentationName, default_payload) do |payload|
|
169
|
+
payload[:result] = @adapter.export(format: format, version: version)
|
170
|
+
end
|
171
|
+
end
|
149
172
|
end
|
150
173
|
end
|
151
174
|
end
|
@@ -5,7 +5,7 @@ module Flipper
|
|
5
5
|
# Internal: Adapter that wraps another adapter with the ability to memoize
|
6
6
|
# adapter calls in memory. Used by flipper dsl and the memoizer middleware
|
7
7
|
# to make it possible to memoize adapter calls for the duration of a request.
|
8
|
-
class Memoizable
|
8
|
+
class Memoizable
|
9
9
|
include ::Flipper::Adapter
|
10
10
|
|
11
11
|
FeaturesKey = :flipper_features
|
@@ -27,7 +27,6 @@ module Flipper
|
|
27
27
|
|
28
28
|
# Public
|
29
29
|
def initialize(adapter, cache = nil)
|
30
|
-
super(adapter)
|
31
30
|
@adapter = adapter
|
32
31
|
@name = :memoizable
|
33
32
|
@cache = cache || {}
|
@@ -128,6 +127,14 @@ module Flipper
|
|
128
127
|
@adapter.disable(feature, gate, thing).tap { expire_feature(feature) }
|
129
128
|
end
|
130
129
|
|
130
|
+
def import(source)
|
131
|
+
@adapter.import(source).tap { cache.clear if memoizing? }
|
132
|
+
end
|
133
|
+
|
134
|
+
def export(format: :json, version: 1)
|
135
|
+
@adapter.export(format: format, version: version)
|
136
|
+
end
|
137
|
+
|
131
138
|
# Internal: Turns local caching on/off.
|
132
139
|
#
|
133
140
|
# value - The Boolean that decides if local caching is on.
|
@@ -141,6 +148,16 @@ module Flipper
|
|
141
148
|
!!@memoize
|
142
149
|
end
|
143
150
|
|
151
|
+
if RUBY_VERSION >= '3.0'
|
152
|
+
def method_missing(name, *args, **kwargs, &block)
|
153
|
+
@adapter.send name, *args, **kwargs, &block
|
154
|
+
end
|
155
|
+
else
|
156
|
+
def method_missing(name, *args, &block)
|
157
|
+
@adapter.send name, *args, &block
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
144
161
|
private
|
145
162
|
|
146
163
|
def key_for(key)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require "flipper/adapter"
|
2
|
+
require "flipper/typecast"
|
1
3
|
require 'concurrent/atomic/read_write_lock'
|
2
4
|
|
3
5
|
module Flipper
|
@@ -14,7 +16,7 @@ module Flipper
|
|
14
16
|
|
15
17
|
# Public
|
16
18
|
def initialize(source = nil)
|
17
|
-
@source =
|
19
|
+
@source = Typecast.features_hash(source)
|
18
20
|
@name = :memory
|
19
21
|
@lock = Concurrent::ReadWriteLock.new
|
20
22
|
end
|
@@ -59,7 +61,7 @@ module Flipper
|
|
59
61
|
end
|
60
62
|
|
61
63
|
def get_all
|
62
|
-
@lock.with_read_lock { @source
|
64
|
+
@lock.with_read_lock { Typecast.features_hash(@source) }
|
63
65
|
end
|
64
66
|
|
65
67
|
# Public
|
@@ -113,9 +115,11 @@ module Flipper
|
|
113
115
|
end
|
114
116
|
|
115
117
|
# Public: a more efficient implementation of import for this adapter
|
116
|
-
def import(
|
117
|
-
|
118
|
+
def import(source)
|
119
|
+
adapter = self.class.from(source)
|
120
|
+
get_all = Typecast.features_hash(adapter.get_all)
|
118
121
|
@lock.with_write_lock { @source.replace(get_all) }
|
122
|
+
true
|
119
123
|
end
|
120
124
|
end
|
121
125
|
end
|
@@ -5,8 +5,8 @@ module Flipper
|
|
5
5
|
# Public: Adapter that wraps another adapter and stores the operations.
|
6
6
|
#
|
7
7
|
# Useful in tests to verify calls and such. Never use outside of testing.
|
8
|
-
class OperationLogger
|
9
|
-
include
|
8
|
+
class OperationLogger
|
9
|
+
include Flipper::Adapter
|
10
10
|
|
11
11
|
class Operation
|
12
12
|
attr_reader :type, :args
|
@@ -18,6 +18,8 @@ module Flipper
|
|
18
18
|
end
|
19
19
|
|
20
20
|
OperationTypes = [
|
21
|
+
:import,
|
22
|
+
:export,
|
21
23
|
:features,
|
22
24
|
:add,
|
23
25
|
:remove,
|
@@ -37,7 +39,6 @@ module Flipper
|
|
37
39
|
|
38
40
|
# Public
|
39
41
|
def initialize(adapter, operations = nil)
|
40
|
-
super(adapter)
|
41
42
|
@adapter = adapter
|
42
43
|
@name = :operation_logger
|
43
44
|
@operations = operations || []
|
@@ -98,6 +99,18 @@ module Flipper
|
|
98
99
|
@adapter.disable(feature, gate, thing)
|
99
100
|
end
|
100
101
|
|
102
|
+
# Public
|
103
|
+
def import(source)
|
104
|
+
@operations << Operation.new(:import, [source])
|
105
|
+
@adapter.import(source)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Public
|
109
|
+
def export(format: :json, version: 1)
|
110
|
+
@operations << Operation.new(:export, [format, version])
|
111
|
+
@adapter.export(format: format, version: version)
|
112
|
+
end
|
113
|
+
|
101
114
|
# Public: Count the number of times a certain operation happened.
|
102
115
|
def count(type)
|
103
116
|
type(type).size
|
data/lib/flipper/dsl.rb
CHANGED
@@ -10,7 +10,7 @@ module Flipper
|
|
10
10
|
# Private: What is being used to instrument all the things.
|
11
11
|
attr_reader :instrumenter
|
12
12
|
|
13
|
-
def_delegators :@adapter, :memoize=, :memoizing
|
13
|
+
def_delegators :@adapter, :memoize=, :memoizing?, :import, :export
|
14
14
|
|
15
15
|
# Public: Returns a new instance of the DSL.
|
16
16
|
#
|
@@ -272,10 +272,6 @@ module Flipper
|
|
272
272
|
adapter.features.map { |name| feature(name) }.to_set
|
273
273
|
end
|
274
274
|
|
275
|
-
def import(flipper)
|
276
|
-
adapter.import(flipper.adapter)
|
277
|
-
end
|
278
|
-
|
279
275
|
# Cloud DSL method that does nothing for open source version.
|
280
276
|
def sync
|
281
277
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "flipper/adapters/memory"
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
class Export
|
5
|
+
attr_reader :contents, :format, :version
|
6
|
+
|
7
|
+
def initialize(contents:, format: :json, version: 1)
|
8
|
+
@contents = contents
|
9
|
+
@format = format
|
10
|
+
@version = version
|
11
|
+
end
|
12
|
+
|
13
|
+
def features
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def adapter
|
18
|
+
@adapter ||= Flipper::Adapters::Memory.new(features)
|
19
|
+
end
|
20
|
+
|
21
|
+
def eql?(other)
|
22
|
+
self.class.eql?(other.class) && @contents == other.contents && @format == other.format && @version == other.version
|
23
|
+
end
|
24
|
+
alias_method :==, :eql?
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "flipper/exporters/json/v1"
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
module Exporter
|
5
|
+
extend self
|
6
|
+
|
7
|
+
FORMATTERS = {
|
8
|
+
json: {
|
9
|
+
1 => Flipper::Exporters::Json::V1,
|
10
|
+
}
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
def build(format: :json, version: 1)
|
14
|
+
FORMATTERS.fetch(format).fetch(version).new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "flipper/export"
|
2
|
+
require "flipper/typecast"
|
3
|
+
|
4
|
+
module Flipper
|
5
|
+
module Exporters
|
6
|
+
module Json
|
7
|
+
# Raised when the contents of the export are not valid.
|
8
|
+
class InvalidError < StandardError; end
|
9
|
+
class JsonError < InvalidError; end
|
10
|
+
|
11
|
+
# Internal: JSON export class that knows how to build features hash
|
12
|
+
# from data.
|
13
|
+
class Export < ::Flipper::Export
|
14
|
+
def initialize(contents:, version: 1)
|
15
|
+
super contents: contents, version: version, format: :json
|
16
|
+
end
|
17
|
+
|
18
|
+
# Public: The features hash identical to calling get_all on adapter.
|
19
|
+
def features
|
20
|
+
@features ||= begin
|
21
|
+
features = JSON.parse(contents).fetch("features")
|
22
|
+
Typecast.features_hash(features)
|
23
|
+
rescue JSON::ParserError
|
24
|
+
raise JsonError
|
25
|
+
rescue
|
26
|
+
raise InvalidError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "json"
|
2
|
+
require "flipper/exporters/json/export"
|
3
|
+
|
4
|
+
module Flipper
|
5
|
+
module Exporters
|
6
|
+
module Json
|
7
|
+
class V1
|
8
|
+
VERSION = 1
|
9
|
+
|
10
|
+
def call(adapter)
|
11
|
+
features = adapter.get_all
|
12
|
+
|
13
|
+
# Convert sets to arrays for json
|
14
|
+
features.each do |feature_key, gates|
|
15
|
+
gates.each do |key, value|
|
16
|
+
case value
|
17
|
+
when Set
|
18
|
+
features[feature_key][key] = value.to_a
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
json = JSON.dump({
|
24
|
+
version: VERSION,
|
25
|
+
features: features,
|
26
|
+
})
|
27
|
+
|
28
|
+
Json::Export.new(contents: json, version: VERSION)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -33,7 +33,15 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
33
33
|
expect(subject.class.ancestors).to include(Flipper::Adapter)
|
34
34
|
end
|
35
35
|
|
36
|
+
it 'knows how to get adapter from source' do
|
37
|
+
adapter = Flipper::Adapters::Memory.new
|
38
|
+
flipper = Flipper.new(adapter)
|
39
|
+
expect(subject.class.from(adapter).class.ancestors).to include(Flipper::Adapter)
|
40
|
+
expect(subject.class.from(flipper).class.ancestors).to include(Flipper::Adapter)
|
41
|
+
end
|
42
|
+
|
36
43
|
it 'returns correct default values for the gates if none are enabled' do
|
44
|
+
expect(subject.get(feature)).to eq(subject.class.default_config)
|
37
45
|
expect(subject.get(feature)).to eq(subject.default_config)
|
38
46
|
end
|
39
47
|
|
@@ -304,4 +312,19 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
304
312
|
subject.enable(feature, boolean_gate, flipper.boolean(true))
|
305
313
|
expect(subject.get(feature)).to eq(subject.default_config.merge(boolean: "true"))
|
306
314
|
end
|
315
|
+
|
316
|
+
it 'can import and export' do
|
317
|
+
adapter = Flipper::Adapters::Memory.new
|
318
|
+
source_flipper = Flipper.new(adapter)
|
319
|
+
source_flipper.enable(:stats)
|
320
|
+
export = adapter.export
|
321
|
+
|
322
|
+
# some adapters cannot import so if they return false lets assert it
|
323
|
+
# didn't happen
|
324
|
+
if subject.import(export)
|
325
|
+
expect(flipper[:stats]).to be_enabled
|
326
|
+
else
|
327
|
+
expect(flipper[:stats]).not_to be_enabled
|
328
|
+
end
|
329
|
+
end
|
307
330
|
end
|
@@ -34,7 +34,16 @@ module Flipper
|
|
34
34
|
assert_includes @adapter.class.ancestors, Flipper::Adapter
|
35
35
|
end
|
36
36
|
|
37
|
+
def test_knows_how_to_get_adapter_from_source
|
38
|
+
adapter = Flipper::Adapters::Memory.new
|
39
|
+
flipper = Flipper.new(adapter)
|
40
|
+
|
41
|
+
assert_includes adapter.class.from(adapter).class.ancestors, Flipper::Adapter
|
42
|
+
assert_includes adapter.class.from(flipper).class.ancestors, Flipper::Adapter
|
43
|
+
end
|
44
|
+
|
37
45
|
def test_returns_correct_default_values_for_gates_if_none_are_enabled
|
46
|
+
assert_equal @adapter.class.default_config, @adapter.get(@feature)
|
38
47
|
assert_equal @adapter.default_config, @adapter.get(@feature)
|
39
48
|
end
|
40
49
|
|
@@ -300,6 +309,21 @@ module Flipper
|
|
300
309
|
assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean(true))
|
301
310
|
assert_equal @adapter.default_config.merge(boolean: "true"), @adapter.get(@feature)
|
302
311
|
end
|
312
|
+
|
313
|
+
def test_can_import_and_export
|
314
|
+
adapter = Flipper::Adapters::Memory.new
|
315
|
+
source_flipper = Flipper.new(adapter)
|
316
|
+
source_flipper.enable(:stats)
|
317
|
+
export = adapter.export
|
318
|
+
|
319
|
+
# some adapters cannot import so if they return false lets assert it
|
320
|
+
# didn't happen
|
321
|
+
if @adapter.import(export)
|
322
|
+
assert @flipper[:stats].enabled?
|
323
|
+
else
|
324
|
+
refute @flipper[:stats].enabled?
|
325
|
+
end
|
326
|
+
end
|
303
327
|
end
|
304
328
|
end
|
305
329
|
end
|
data/lib/flipper/typecast.rb
CHANGED
@@ -62,5 +62,22 @@ module Flipper
|
|
62
62
|
raise ArgumentError, "#{value.inspect} cannot be converted to a set"
|
63
63
|
end
|
64
64
|
end
|
65
|
+
|
66
|
+
def self.features_hash(source)
|
67
|
+
normalized_source = {}
|
68
|
+
(source || {}).each do |feature_key, gates|
|
69
|
+
normalized_source[feature_key] ||= {}
|
70
|
+
gates.each do |gate_key, value|
|
71
|
+
normalized_value = case value
|
72
|
+
when Array, Set
|
73
|
+
value.to_set
|
74
|
+
else
|
75
|
+
value ? value.to_s : value
|
76
|
+
end
|
77
|
+
normalized_source[feature_key][gate_key.to_sym] = normalized_value
|
78
|
+
end
|
79
|
+
end
|
80
|
+
normalized_source
|
81
|
+
end
|
65
82
|
end
|
66
83
|
end
|
data/lib/flipper/version.rb
CHANGED
data/lib/flipper.rb
CHANGED
@@ -64,7 +64,7 @@ module Flipper
|
|
64
64
|
:enable_percentage_of_time, :disable_percentage_of_time,
|
65
65
|
:time, :percentage_of_time,
|
66
66
|
:features, :feature, :[], :preload, :preload_all,
|
67
|
-
:adapter, :add, :exist?, :remove, :import,
|
67
|
+
:adapter, :add, :exist?, :remove, :import, :export,
|
68
68
|
:memoize=, :memoizing?,
|
69
69
|
:sync, :sync_secret # For Flipper::Cloud. Will error for OSS Flipper.
|
70
70
|
|
@@ -165,5 +165,6 @@ require 'flipper/types/percentage'
|
|
165
165
|
require 'flipper/types/percentage_of_actors'
|
166
166
|
require 'flipper/types/percentage_of_time'
|
167
167
|
require 'flipper/typecast'
|
168
|
+
require 'flipper/version'
|
168
169
|
|
169
170
|
require "flipper/railtie" if defined?(Rails::Railtie)
|