flipper 0.26.2 → 0.27.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +4 -0
  3. data/Gemfile +2 -3
  4. data/examples/api/basic.ru +3 -4
  5. data/examples/api/custom_memoized.ru +3 -4
  6. data/examples/api/memoized.ru +3 -4
  7. data/lib/flipper/adapter.rb +23 -7
  8. data/lib/flipper/adapters/http.rb +11 -3
  9. data/lib/flipper/adapters/instrumented.rb +25 -2
  10. data/lib/flipper/adapters/memoizable.rb +19 -2
  11. data/lib/flipper/adapters/memory.rb +8 -4
  12. data/lib/flipper/adapters/operation_logger.rb +16 -3
  13. data/lib/flipper/dsl.rb +1 -5
  14. data/lib/flipper/export.rb +26 -0
  15. data/lib/flipper/exporter.rb +17 -0
  16. data/lib/flipper/exporters/json/export.rb +32 -0
  17. data/lib/flipper/exporters/json/v1.rb +33 -0
  18. data/lib/flipper/instrumentation/subscriber.rb +8 -0
  19. data/lib/flipper/spec/shared_adapter_specs.rb +23 -0
  20. data/lib/flipper/test/shared_adapter_test.rb +24 -0
  21. data/lib/flipper/typecast.rb +17 -0
  22. data/lib/flipper/version.rb +1 -1
  23. data/lib/flipper.rb +1 -1
  24. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  25. data/spec/flipper/adapter_spec.rb +29 -2
  26. data/spec/flipper/adapters/http_spec.rb +25 -3
  27. data/spec/flipper/adapters/instrumented_spec.rb +28 -10
  28. data/spec/flipper/adapters/memoizable_spec.rb +30 -10
  29. data/spec/flipper/adapters/operation_logger_spec.rb +29 -10
  30. data/spec/flipper/dsl_spec.rb +20 -3
  31. data/spec/flipper/export_spec.rb +13 -0
  32. data/spec/flipper/exporter_spec.rb +16 -0
  33. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  34. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  35. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -0
  36. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -0
  37. data/spec/flipper/typecast_spec.rb +79 -0
  38. data/spec/flipper_spec.rb +7 -1
  39. data/spec/support/skippable.rb +18 -0
  40. metadata +18 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 670e45600b4c72208ed69da2792d2d4e1ac11d7a54f19c45c4b448ba6dacc404
4
- data.tar.gz: 56a6ef12c569a7392212953604ed103a1fb187b57d5f827ea141a0d5eafc5ee1
3
+ metadata.gz: 202d50faf999fd9ed12d91ef7f160f786892b9476ad86b815fd4be5390cf8992
4
+ data.tar.gz: 1f59767d2f210820f488e97a0388f84aa2d474ac7b573e1650a69104d15f384a
5
5
  SHA512:
6
- metadata.gz: a09649689708c91afbdc3e9f4eded2ec29ae1b3ac06a98f28197dcc4d3dd65983f73e0e9e44994d78b0672afbea1350ff7256dc12b7e5bb426961bfce858b4f6
7
- data.tar.gz: '08eb67c5e5df45e734e8b6e50c6fd20cfbdf28dbdb8ba1a3e2d2af084250b9cd404fa21cc778989527d245154e05f5abc49aaa12a95b2ef7b12daf4bbea41830'
6
+ metadata.gz: f4b8079927fe805c7c6200694c6b9d9b020539cf1835d62a713724adc1483c48399a3f0073c969d4f9619b0362d615c21813ed79470172ea7037e62cfbb791db
7
+ data.tar.gz: 8a389963c5386d4d9ef13be89e06f6f38e959dcb0c427b7950ffee628828d627ff567581da375d3e9eb8b3501ba7e24d0f4b82acdf6a04599fb0cf7a2385f250
data/Changelog.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 0.27.0
6
+
7
+ * 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.
8
+
5
9
  ## 0.26.2
6
10
 
7
11
  * 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', '~> 0.6.3'
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.8'
16
+ gem 'minitest', '~> 5.18'
18
17
  gem 'minitest-documentation'
19
18
  gem 'webmock', '~> 3.0'
20
19
  gem 'ice_age'
@@ -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
@@ -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
  }
@@ -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
- # Returns result of Synchronizer#call.
47
- def import(source_adapter)
48
- Adapters::Sync::Synchronizer.new(self, source_adapter, raise: true).call
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 < SimpleDelegator
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 < SimpleDelegator
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 = Hash.new.update(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.to_h }
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(source_adapter)
117
- get_all = source_adapter.get_all
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 < SimpleDelegator
9
- include ::Flipper::Adapter
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
@@ -72,6 +72,14 @@ module Flipper
72
72
  update_timer "flipper.adapter.#{adapter_name}.#{operation}"
73
73
  end
74
74
 
75
+ def update_poller_metrics
76
+ # noop
77
+ end
78
+
79
+ def update_synchronizer_call_metrics
80
+ # noop
81
+ end
82
+
75
83
  QUESTION_MARK = '?'.freeze
76
84
 
77
85
  # Private
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.26.2'.freeze
2
+ VERSION = '0.27.0'.freeze
3
3
  end
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