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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +8 -0
  3. data/Gemfile +2 -3
  4. data/Rakefile +3 -3
  5. data/examples/api/basic.ru +3 -4
  6. data/examples/api/custom_memoized.ru +3 -4
  7. data/examples/api/memoized.ru +3 -4
  8. data/lib/flipper/adapter.rb +23 -7
  9. data/lib/flipper/adapters/http.rb +11 -3
  10. data/lib/flipper/adapters/instrumented.rb +25 -2
  11. data/lib/flipper/adapters/memoizable.rb +19 -2
  12. data/lib/flipper/adapters/memory.rb +8 -4
  13. data/lib/flipper/adapters/operation_logger.rb +16 -3
  14. data/lib/flipper/dsl.rb +1 -5
  15. data/lib/flipper/export.rb +26 -0
  16. data/lib/flipper/exporter.rb +17 -0
  17. data/lib/flipper/exporters/json/export.rb +32 -0
  18. data/lib/flipper/exporters/json/v1.rb +33 -0
  19. data/lib/flipper/instrumentation/subscriber.rb +8 -0
  20. data/lib/flipper/spec/shared_adapter_specs.rb +23 -0
  21. data/lib/flipper/test/shared_adapter_test.rb +24 -0
  22. data/lib/flipper/typecast.rb +17 -0
  23. data/lib/flipper/version.rb +1 -1
  24. data/lib/flipper.rb +2 -1
  25. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  26. data/spec/flipper/adapter_spec.rb +29 -2
  27. data/spec/flipper/adapters/http_spec.rb +25 -3
  28. data/spec/flipper/adapters/instrumented_spec.rb +28 -10
  29. data/spec/flipper/adapters/memoizable_spec.rb +30 -10
  30. data/spec/flipper/adapters/operation_logger_spec.rb +29 -10
  31. data/spec/flipper/dsl_spec.rb +20 -3
  32. data/spec/flipper/export_spec.rb +13 -0
  33. data/spec/flipper/exporter_spec.rb +16 -0
  34. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  35. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  36. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -0
  37. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -0
  38. data/spec/flipper/typecast_spec.rb +79 -0
  39. data/spec/flipper_spec.rb +7 -1
  40. data/spec/support/skippable.rb +18 -0
  41. 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: ba1d158db684bce01872e3f32c81f522d32086a78af94a78a6266422079a3473
4
+ data.tar.gz: 9a7ea456415d5fd73d5adcb66bb77c4701a2a6e6b3ab94d5c89af52202907e7c
5
5
  SHA512:
6
- metadata.gz: a09649689708c91afbdc3e9f4eded2ec29ae1b3ac06a98f28197dcc4d3dd65983f73e0e9e44994d78b0672afbea1350ff7256dc12b7e5bb426961bfce858b4f6
7
- data.tar.gz: '08eb67c5e5df45e734e8b6e50c6fd20cfbdf28dbdb8ba1a3e2d2af084250b9cd404fa21cc778989527d245154e05f5abc49aaa12a95b2ef7b12daf4bbea41830'
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', '~> 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'
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}"
@@ -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.1'.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
 
@@ -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)