flipper 0.26.2 → 0.27.1
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|