ldclient-rb 5.4.3 → 5.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +33 -6
  3. data/CHANGELOG.md +19 -0
  4. data/CONTRIBUTING.md +0 -12
  5. data/Gemfile.lock +22 -3
  6. data/README.md +41 -35
  7. data/ldclient-rb.gemspec +4 -3
  8. data/lib/ldclient-rb.rb +9 -1
  9. data/lib/ldclient-rb/cache_store.rb +1 -0
  10. data/lib/ldclient-rb/config.rb +201 -90
  11. data/lib/ldclient-rb/evaluation.rb +56 -8
  12. data/lib/ldclient-rb/event_summarizer.rb +3 -0
  13. data/lib/ldclient-rb/events.rb +16 -0
  14. data/lib/ldclient-rb/expiring_cache.rb +1 -0
  15. data/lib/ldclient-rb/file_data_source.rb +18 -13
  16. data/lib/ldclient-rb/flags_state.rb +3 -2
  17. data/lib/ldclient-rb/impl.rb +13 -0
  18. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +158 -0
  19. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +228 -0
  20. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +155 -0
  21. data/lib/ldclient-rb/impl/store_client_wrapper.rb +47 -0
  22. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
  23. data/lib/ldclient-rb/in_memory_store.rb +15 -4
  24. data/lib/ldclient-rb/integrations.rb +55 -0
  25. data/lib/ldclient-rb/integrations/consul.rb +38 -0
  26. data/lib/ldclient-rb/integrations/dynamodb.rb +47 -0
  27. data/lib/ldclient-rb/integrations/redis.rb +55 -0
  28. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +230 -0
  29. data/lib/ldclient-rb/interfaces.rb +153 -0
  30. data/lib/ldclient-rb/ldclient.rb +135 -77
  31. data/lib/ldclient-rb/memoized_value.rb +2 -0
  32. data/lib/ldclient-rb/newrelic.rb +1 -0
  33. data/lib/ldclient-rb/non_blocking_thread_pool.rb +3 -3
  34. data/lib/ldclient-rb/polling.rb +1 -0
  35. data/lib/ldclient-rb/redis_store.rb +24 -190
  36. data/lib/ldclient-rb/requestor.rb +3 -2
  37. data/lib/ldclient-rb/simple_lru_cache.rb +1 -0
  38. data/lib/ldclient-rb/stream.rb +22 -10
  39. data/lib/ldclient-rb/user_filter.rb +1 -0
  40. data/lib/ldclient-rb/util.rb +1 -0
  41. data/lib/ldclient-rb/version.rb +1 -1
  42. data/scripts/gendocs.sh +12 -0
  43. data/spec/feature_store_spec_base.rb +173 -72
  44. data/spec/file_data_source_spec.rb +2 -2
  45. data/spec/http_util.rb +103 -0
  46. data/spec/in_memory_feature_store_spec.rb +1 -1
  47. data/spec/integrations/consul_feature_store_spec.rb +41 -0
  48. data/spec/integrations/dynamodb_feature_store_spec.rb +104 -0
  49. data/spec/integrations/store_wrapper_spec.rb +276 -0
  50. data/spec/ldclient_spec.rb +83 -4
  51. data/spec/redis_feature_store_spec.rb +25 -16
  52. data/spec/requestor_spec.rb +44 -38
  53. data/spec/stream_spec.rb +18 -18
  54. metadata +55 -33
  55. data/lib/sse_client.rb +0 -4
  56. data/lib/sse_client/backoff.rb +0 -38
  57. data/lib/sse_client/sse_client.rb +0 -171
  58. data/lib/sse_client/sse_events.rb +0 -67
  59. data/lib/sse_client/streaming_http.rb +0 -199
  60. data/spec/sse_client/sse_client_spec.rb +0 -177
  61. data/spec/sse_client/sse_events_spec.rb +0 -100
  62. data/spec/sse_client/sse_shared.rb +0 -82
  63. data/spec/sse_client/streaming_http_spec.rb +0 -263
@@ -2,6 +2,8 @@
2
2
  module LaunchDarkly
3
3
  # Simple implementation of a thread-safe memoized value whose generator function will never be
4
4
  # run more than once, and whose value can be overridden by explicit assignment.
5
+ # Note that we no longer use this class and it will be removed in a future version.
6
+ # @private
5
7
  class MemoizedValue
6
8
  def initialize(&generator)
7
9
  @generator = generator
@@ -1,4 +1,5 @@
1
1
  module LaunchDarkly
2
+ # @private
2
3
  class LDNewRelic
3
4
  begin
4
5
  require "newrelic_rpm"
@@ -3,10 +3,10 @@ require "concurrent/atomics"
3
3
  require "concurrent/executors"
4
4
  require "thread"
5
5
 
6
- # Simple wrapper for a FixedThreadPool that rejects new jobs if all the threads are busy, rather
7
- # than blocking. Also provides a way to wait for all jobs to finish without shutting down.
8
-
9
6
  module LaunchDarkly
7
+ # Simple wrapper for a FixedThreadPool that rejects new jobs if all the threads are busy, rather
8
+ # than blocking. Also provides a way to wait for all jobs to finish without shutting down.
9
+ # @private
10
10
  class NonBlockingThreadPool
11
11
  def initialize(capacity)
12
12
  @capacity = capacity
@@ -2,6 +2,7 @@ require "concurrent/atomics"
2
2
  require "thread"
3
3
 
4
4
  module LaunchDarkly
5
+ # @private
5
6
  class PollingProcessor
6
7
  def initialize(config, requestor)
7
8
  @config = config
@@ -1,5 +1,5 @@
1
- require "concurrent/atomics"
2
- require "json"
1
+ require "ldclient-rb/interfaces"
2
+ require "ldclient-rb/impl/integrations/redis_impl"
3
3
 
4
4
  module LaunchDarkly
5
5
  #
@@ -12,14 +12,16 @@ module LaunchDarkly
12
12
  # installed. Then, create an instance and store it in the `feature_store` property
13
13
  # of your client configuration.
14
14
  #
15
+ # @deprecated Use the factory method in {LaunchDarkly::Integrations::Redis} instead. This specific
16
+ # implementation class may be changed or removed in the future.
17
+ #
15
18
  class RedisFeatureStore
16
- begin
17
- require "redis"
18
- require "connection_pool"
19
- REDIS_ENABLED = true
20
- rescue ScriptError, StandardError
21
- REDIS_ENABLED = false
22
- end
19
+ include LaunchDarkly::Interfaces::FeatureStore
20
+
21
+ # Note that this class is now just a facade around CachingStoreWrapper, which is in turn delegating
22
+ # to RedisFeatureStoreCore where the actual database logic is. This class was retained for historical
23
+ # reasons, so that existing code can still call RedisFeatureStore.new. In the future, we will migrate
24
+ # away from exposing these concrete classes and use factory methods instead.
23
25
 
24
26
  #
25
27
  # Constructor for a RedisFeatureStore instance.
@@ -30,45 +32,13 @@ module LaunchDarkly
30
32
  # @option opts [String] :prefix namespace prefix to add to all hash keys used by LaunchDarkly
31
33
  # @option opts [Logger] :logger a `Logger` instance; defaults to `Config.default_logger`
32
34
  # @option opts [Integer] :max_connections size of the Redis connection pool
33
- # @option opts [Integer] :expiration expiration time for the in-memory cache, in seconds; 0 for no local caching
35
+ # @option opts [Integer] :expiration_seconds expiration time for the in-memory cache, in seconds; 0 for no local caching
34
36
  # @option opts [Integer] :capacity maximum number of feature flags (or related objects) to cache locally
35
- # @option opts [Object] :pool custom connection pool, used for testing only
37
+ # @option opts [Object] :pool custom connection pool, if desired
36
38
  #
37
39
  def initialize(opts = {})
38
- if !REDIS_ENABLED
39
- raise RuntimeError.new("can't use RedisFeatureStore because one of these gems is missing: redis, connection_pool")
40
- end
41
- @redis_opts = opts[:redis_opts] || Hash.new
42
- if opts[:redis_url]
43
- @redis_opts[:url] = opts[:redis_url]
44
- end
45
- if !@redis_opts.include?(:url)
46
- @redis_opts[:url] = RedisFeatureStore.default_redis_url
47
- end
48
- max_connections = opts[:max_connections] || 16
49
- @pool = opts[:pool] || ConnectionPool.new(size: max_connections) do
50
- Redis.new(@redis_opts)
51
- end
52
- @prefix = opts[:prefix] || RedisFeatureStore.default_prefix
53
- @logger = opts[:logger] || Config.default_logger
54
-
55
- expiration_seconds = opts[:expiration] || 15
56
- capacity = opts[:capacity] || 1000
57
- if expiration_seconds > 0
58
- @cache = ExpiringCache.new(capacity, expiration_seconds)
59
- else
60
- @cache = nil
61
- end
62
-
63
- @stopped = Concurrent::AtomicBoolean.new(false)
64
- @inited = MemoizedValue.new {
65
- query_inited
66
- }
67
-
68
- with_connection do |redis|
69
- @logger.info("RedisFeatureStore: using Redis instance at #{redis.connection[:host]}:#{redis.connection[:port]} \
70
- and prefix: #{@prefix}")
71
- end
40
+ core = LaunchDarkly::Impl::Integrations::Redis::RedisFeatureStoreCore.new(opts)
41
+ @wrapper = LaunchDarkly::Integrations::Util::CachingStoreWrapper.new(core, opts)
72
42
  end
73
43
 
74
44
  #
@@ -76,178 +46,42 @@ and prefix: #{@prefix}")
76
46
  # running at `localhost` with its default port.
77
47
  #
78
48
  def self.default_redis_url
79
- 'redis://localhost:6379/0'
49
+ LaunchDarkly::Integrations::Redis::default_redis_url
80
50
  end
81
51
 
82
52
  #
83
53
  # Default value for the `prefix` constructor parameter.
84
54
  #
85
55
  def self.default_prefix
86
- 'launchdarkly'
56
+ LaunchDarkly::Integrations::Redis::default_prefix
87
57
  end
88
58
 
89
59
  def get(kind, key)
90
- f = @cache.nil? ? nil : @cache[cache_key(kind, key)]
91
- if f.nil?
92
- @logger.debug { "RedisFeatureStore: no cache hit for #{key} in '#{kind[:namespace]}', requesting from Redis" }
93
- f = with_connection do |redis|
94
- begin
95
- get_redis(kind, redis, key.to_sym)
96
- rescue => e
97
- @logger.error { "RedisFeatureStore: could not retrieve #{key} from Redis in '#{kind[:namespace]}', with error: #{e}" }
98
- nil
99
- end
100
- end
101
- end
102
- if f.nil?
103
- @logger.debug { "RedisFeatureStore: #{key} not found in '#{kind[:namespace]}'" }
104
- nil
105
- elsif f[:deleted]
106
- @logger.debug { "RedisFeatureStore: #{key} was deleted in '#{kind[:namespace]}', returning nil" }
107
- nil
108
- else
109
- f
110
- end
60
+ @wrapper.get(kind, key)
111
61
  end
112
62
 
113
63
  def all(kind)
114
- fs = {}
115
- with_connection do |redis|
116
- begin
117
- hashfs = redis.hgetall(items_key(kind))
118
- rescue => e
119
- @logger.error { "RedisFeatureStore: could not retrieve all '#{kind[:namespace]}' items from Redis with error: #{e}; returning none" }
120
- hashfs = {}
121
- end
122
- hashfs.each do |k, jsonItem|
123
- f = JSON.parse(jsonItem, symbolize_names: true)
124
- if !f[:deleted]
125
- fs[k.to_sym] = f
126
- end
127
- end
128
- end
129
- fs
64
+ @wrapper.all(kind)
130
65
  end
131
66
 
132
67
  def delete(kind, key, version)
133
- update_with_versioning(kind, { key: key, version: version, deleted: true })
68
+ @wrapper.delete(kind, key, version)
134
69
  end
135
70
 
136
71
  def init(all_data)
137
- @cache.clear if !@cache.nil?
138
- count = 0
139
- with_connection do |redis|
140
- all_data.each do |kind, items|
141
- begin
142
- redis.multi do |multi|
143
- multi.del(items_key(kind))
144
- count = count + items.count
145
- items.each { |key, item|
146
- redis.hset(items_key(kind), key, item.to_json)
147
- }
148
- end
149
- items.each { |key, item|
150
- put_cache(kind, key.to_sym, item)
151
- }
152
- rescue => e
153
- @logger.error { "RedisFeatureStore: could not initialize '#{kind[:namespace]}' in Redis, error: #{e}" }
154
- end
155
- end
156
- end
157
- @inited.set(true)
158
- @logger.info { "RedisFeatureStore: initialized with #{count} items" }
72
+ @wrapper.init(all_data)
159
73
  end
160
74
 
161
75
  def upsert(kind, item)
162
- update_with_versioning(kind, item)
76
+ @wrapper.upsert(kind, item)
163
77
  end
164
78
 
165
79
  def initialized?
166
- @inited.get
80
+ @wrapper.initialized?
167
81
  end
168
82
 
169
83
  def stop
170
- if @stopped.make_true
171
- @pool.shutdown { |redis| redis.close }
172
- @cache.clear if !@cache.nil?
173
- end
174
- end
175
-
176
- private
177
-
178
- # exposed for testing
179
- def before_update_transaction(base_key, key)
180
- end
181
-
182
- def items_key(kind)
183
- @prefix + ":" + kind[:namespace]
184
- end
185
-
186
- def cache_key(kind, key)
187
- kind[:namespace] + ":" + key.to_s
188
- end
189
-
190
- def with_connection
191
- @pool.with { |redis| yield(redis) }
192
- end
193
-
194
- def get_redis(kind, redis, key)
195
- begin
196
- json_item = redis.hget(items_key(kind), key)
197
- if json_item
198
- item = JSON.parse(json_item, symbolize_names: true)
199
- put_cache(kind, key, item)
200
- item
201
- else
202
- nil
203
- end
204
- rescue => e
205
- @logger.error { "RedisFeatureStore: could not retrieve #{key} from Redis, error: #{e}" }
206
- nil
207
- end
208
- end
209
-
210
- def put_cache(kind, key, value)
211
- @cache[cache_key(kind, key)] = value if !@cache.nil?
212
- end
213
-
214
- def update_with_versioning(kind, new_item)
215
- base_key = items_key(kind)
216
- key = new_item[:key]
217
- try_again = true
218
- while try_again
219
- try_again = false
220
- with_connection do |redis|
221
- redis.watch(base_key) do
222
- old_item = get_redis(kind, redis, key)
223
- before_update_transaction(base_key, key)
224
- if old_item.nil? || old_item[:version] < new_item[:version]
225
- begin
226
- result = redis.multi do |multi|
227
- multi.hset(base_key, key, new_item.to_json)
228
- end
229
- if result.nil?
230
- @logger.debug { "RedisFeatureStore: concurrent modification detected, retrying" }
231
- try_again = true
232
- else
233
- put_cache(kind, key.to_sym, new_item)
234
- end
235
- rescue => e
236
- @logger.error { "RedisFeatureStore: could not store #{key} in Redis, error: #{e}" }
237
- end
238
- else
239
- action = new_item[:deleted] ? "delete" : "update"
240
- @logger.warn { "RedisFeatureStore: attempted to #{action} #{key} version: #{old_item[:version]} \
241
- in '#{kind[:namespace]}' with a version that is the same or older: #{new_item[:version]}" }
242
- end
243
- redis.unwatch
244
- end
245
- end
246
- end
247
- end
248
-
249
- def query_inited
250
- with_connection { |redis| redis.exists(items_key(FEATURES)) }
84
+ @wrapper.stop
251
85
  end
252
86
  end
253
87
  end
@@ -3,7 +3,7 @@ require "net/http/persistent"
3
3
  require "faraday/http_cache"
4
4
 
5
5
  module LaunchDarkly
6
-
6
+ # @private
7
7
  class UnexpectedResponseError < StandardError
8
8
  def initialize(status)
9
9
  @status = status
@@ -14,12 +14,13 @@ module LaunchDarkly
14
14
  end
15
15
  end
16
16
 
17
+ # @private
17
18
  class Requestor
18
19
  def initialize(sdk_key, config)
19
20
  @sdk_key = sdk_key
20
21
  @config = config
21
22
  @client = Faraday.new do |builder|
22
- builder.use :http_cache, store: @config.cache_store
23
+ builder.use :http_cache, store: @config.cache_store, serializer: Marshal
23
24
 
24
25
  builder.adapter :net_http_persistent
25
26
  end
@@ -2,6 +2,7 @@
2
2
  module LaunchDarkly
3
3
  # A non-thread-safe implementation of a LRU cache set with only add and reset methods.
4
4
  # Based on https://github.com/SamSaffron/lru_redux/blob/master/lib/lru_redux/cache.rb
5
+ # @private
5
6
  class SimpleLRUCacheSet
6
7
  def initialize(capacity)
7
8
  @values = {}
@@ -1,20 +1,28 @@
1
1
  require "concurrent/atomics"
2
2
  require "json"
3
- require "sse_client"
3
+ require "ld-eventsource"
4
4
 
5
5
  module LaunchDarkly
6
+ # @private
6
7
  PUT = :put
8
+ # @private
7
9
  PATCH = :patch
10
+ # @private
8
11
  DELETE = :delete
12
+ # @private
9
13
  INDIRECT_PUT = :'indirect/put'
14
+ # @private
10
15
  INDIRECT_PATCH = :'indirect/patch'
16
+ # @private
11
17
  READ_TIMEOUT_SECONDS = 300 # 5 minutes; the stream should send a ping every 3 minutes
12
18
 
19
+ # @private
13
20
  KEY_PATHS = {
14
21
  FEATURES => "/flags/",
15
22
  SEGMENTS => "/segments/"
16
23
  }
17
24
 
25
+ # @private
18
26
  class StreamProcessor
19
27
  def initialize(sdk_key, config, requestor)
20
28
  @sdk_key = sdk_key
@@ -46,15 +54,18 @@ module LaunchDarkly
46
54
  read_timeout: READ_TIMEOUT_SECONDS,
47
55
  logger: @config.logger
48
56
  }
49
- @es = SSE::SSEClient.new(@config.stream_uri + "/all", opts) do |conn|
50
- conn.on_event { |event| process_message(event, event.type) }
57
+ @es = SSE::Client.new(@config.stream_uri + "/all", **opts) do |conn|
58
+ conn.on_event { |event| process_message(event) }
51
59
  conn.on_error { |err|
52
- status = err[:status_code]
53
- message = Util.http_error_message(status, "streaming connection", "will retry")
54
- @config.logger.error { "[LDClient] #{message}" }
55
- if !Util.http_error_recoverable?(status)
56
- @ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
57
- stop
60
+ case err
61
+ when SSE::Errors::HTTPStatusError
62
+ status = err.status
63
+ message = Util.http_error_message(status, "streaming connection", "will retry")
64
+ @config.logger.error { "[LDClient] #{message}" }
65
+ if !Util.http_error_recoverable?(status)
66
+ @ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
67
+ stop
68
+ end
58
69
  end
59
70
  }
60
71
  end
@@ -71,7 +82,8 @@ module LaunchDarkly
71
82
 
72
83
  private
73
84
 
74
- def process_message(message, method)
85
+ def process_message(message)
86
+ method = message.type
75
87
  @config.logger.debug { "[LDClient] Stream received #{method} message: #{message.data}" }
76
88
  if method == PUT
77
89
  message = JSON.parse(message.data, symbolize_names: true)
@@ -2,6 +2,7 @@ require "json"
2
2
  require "set"
3
3
 
4
4
  module LaunchDarkly
5
+ # @private
5
6
  class UserFilter
6
7
  def initialize(config)
7
8
  @all_attributes_private = config.all_attributes_private
@@ -1,5 +1,6 @@
1
1
 
2
2
  module LaunchDarkly
3
+ # @private
3
4
  module Util
4
5
  def self.log_exception(logger, message, exc)
5
6
  logger.error { "[LDClient] #{message}: #{exc.inspect}" }
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "5.4.3"
2
+ VERSION = "5.5.0"
3
3
  end
@@ -0,0 +1,12 @@
1
+ #!/bin/bash
2
+
3
+ # Use this script to generate documentation locally in ./doc so it can be proofed before release.
4
+ # After release, documentation will be visible at https://www.rubydoc.info/gems/ldclient-rb
5
+
6
+ gem install --conservative yard
7
+ gem install --conservative redcarpet # provides Markdown formatting
8
+
9
+ # yard doesn't seem to do recursive directories, even though Ruby's Dir.glob supposedly recurses for "**"
10
+ PATHS="lib/*.rb lib/**/*.rb lib/**/**/*.rb lib/**/**/**/*.rb"
11
+
12
+ yard doc --no-private --markup markdown --markup-provider redcarpet --embed-mixins $PATHS - README.md
@@ -1,112 +1,213 @@
1
1
  require "spec_helper"
2
2
 
3
- RSpec.shared_examples "feature_store" do |create_store_method|
3
+ shared_examples "feature_store" do |create_store_method, clear_data_method|
4
4
 
5
- let(:feature0) {
5
+ # Rather than testing with feature flag or segment data, we'll use this fake data kind
6
+ # to make it clear that feature stores need to be able to handle arbitrary data.
7
+ let(:things_kind) { { namespace: "things" } }
8
+
9
+ let(:key1) { "thing1" }
10
+ let(:thing1) {
6
11
  {
7
- key: "test-feature-flag",
12
+ key: key1,
13
+ name: "Thing 1",
8
14
  version: 11,
9
- on: true,
10
- prerequisites: [],
11
- salt: "718ea30a918a4eba8734b57ab1a93227",
12
- sel: "fe1244e5378c4f99976c9634e33667c6",
13
- targets: [
14
- {
15
- values: [ "alice" ],
16
- variation: 0
17
- },
18
- {
19
- values: [ "bob" ],
20
- variation: 1
21
- }
22
- ],
23
- rules: [],
24
- fallthrough: { variation: 0 },
25
- offVariation: 1,
26
- variations: [ true, false ],
27
15
  deleted: false
28
16
  }
29
17
  }
30
- let(:key0) { feature0[:key].to_sym }
18
+ let(:unused_key) { "no" }
19
+
20
+ let(:create_store) { create_store_method } # just to avoid a scope issue
21
+ let(:clear_data) { clear_data_method }
22
+
23
+ def with_store(opts = {})
24
+ s = create_store.call(opts)
25
+ begin
26
+ yield s
27
+ ensure
28
+ s.stop
29
+ end
30
+ end
31
+
32
+ def with_inited_store(things)
33
+ things_hash = {}
34
+ things.each { |thing| things_hash[thing[:key].to_sym] = thing }
31
35
 
32
- let!(:store) do
33
- s = create_store_method.call()
34
- s.init(LaunchDarkly::FEATURES => { key0 => feature0 })
35
- s
36
+ with_store do |s|
37
+ s.init({ things_kind => things_hash })
38
+ yield s
39
+ end
36
40
  end
37
41
 
38
42
  def new_version_plus(f, deltaVersion, attrs = {})
39
- f1 = f.clone
40
- f1[:version] = f[:version] + deltaVersion
41
- f1.update(attrs)
42
- f1
43
+ f.clone.merge({ version: f[:version] + deltaVersion }).merge(attrs)
43
44
  end
44
45
 
46
+ before(:each) do
47
+ clear_data.call if !clear_data.nil?
48
+ end
45
49
 
46
- it "is initialized" do
47
- expect(store.initialized?).to eq true
50
+ # This block of tests is only run if the clear_data method is defined, meaning that this is a persistent store
51
+ # that operates on a database that can be shared with other store instances (as opposed to the in-memory store,
52
+ # which has its own private storage).
53
+ if !clear_data_method.nil?
54
+ it "is not initialized by default" do
55
+ with_store do |store|
56
+ expect(store.initialized?).to eq false
57
+ end
58
+ end
59
+
60
+ it "can detect if another instance has initialized the store" do
61
+ with_store do |store1|
62
+ store1.init({})
63
+ with_store do |store2|
64
+ expect(store2.initialized?).to eq true
65
+ end
66
+ end
67
+ end
68
+
69
+ it "can read data written by another instance" do
70
+ with_store do |store1|
71
+ store1.init({ things_kind => { key1.to_sym => thing1 } })
72
+ with_store do |store2|
73
+ expect(store2.get(things_kind, key1)).to eq thing1
74
+ end
75
+ end
76
+ end
77
+
78
+ it "is independent from other stores with different prefixes" do
79
+ with_store({ prefix: "a" }) do |store_a|
80
+ store_a.init({ things_kind => { key1.to_sym => thing1 } })
81
+ with_store({ prefix: "b" }) do |store_b|
82
+ store_b.init({ things_kind => {} })
83
+ end
84
+ with_store({ prefix: "b" }) do |store_b1| # this ensures we're not just reading cached data
85
+ expect(store_b1.get(things_kind, key1)).to be_nil
86
+ expect(store_a.get(things_kind, key1)).to eq thing1
87
+ end
88
+ end
89
+ end
48
90
  end
49
91
 
50
- it "can get existing feature with symbol key" do
51
- expect(store.get(LaunchDarkly::FEATURES, key0)).to eq feature0
92
+ it "is initialized after calling init" do
93
+ with_inited_store([]) do |store|
94
+ expect(store.initialized?).to eq true
95
+ end
52
96
  end
53
97
 
54
- it "can get existing feature with string key" do
55
- expect(store.get(LaunchDarkly::FEATURES, key0.to_s)).to eq feature0
98
+ it "can get existing item with symbol key" do
99
+ with_inited_store([ thing1 ]) do |store|
100
+ expect(store.get(things_kind, key1.to_sym)).to eq thing1
101
+ end
56
102
  end
57
103
 
58
- it "gets nil for nonexisting feature" do
59
- expect(store.get(LaunchDarkly::FEATURES, 'nope')).to be_nil
104
+ it "can get existing item with string key" do
105
+ with_inited_store([ thing1 ]) do |store|
106
+ expect(store.get(things_kind, key1.to_s)).to eq thing1
107
+ end
60
108
  end
61
109
 
62
- it "can get all features" do
63
- feature1 = feature0.clone
64
- feature1[:key] = "test-feature-flag1"
65
- feature1[:version] = 5
66
- feature1[:on] = false
67
- store.upsert(LaunchDarkly::FEATURES, feature1)
68
- expect(store.all(LaunchDarkly::FEATURES)).to eq ({ key0 => feature0, :"test-feature-flag1" => feature1 })
110
+ it "gets nil for nonexisting item" do
111
+ with_inited_store([ thing1 ]) do |store|
112
+ expect(store.get(things_kind, unused_key)).to be_nil
113
+ end
69
114
  end
70
115
 
71
- it "can add new feature" do
72
- feature1 = feature0.clone
73
- feature1[:key] = "test-feature-flag1"
74
- feature1[:version] = 5
75
- feature1[:on] = false
76
- store.upsert(LaunchDarkly::FEATURES, feature1)
77
- expect(store.get(LaunchDarkly::FEATURES, :"test-feature-flag1")).to eq feature1
116
+ it "returns nil for deleted item" do
117
+ deleted_thing = thing1.clone.merge({ deleted: true })
118
+ with_inited_store([ deleted_thing ]) do |store|
119
+ expect(store.get(things_kind, key1)).to be_nil
120
+ end
78
121
  end
79
122
 
80
- it "can update feature with newer version" do
81
- f1 = new_version_plus(feature0, 1, { on: !feature0[:on] })
82
- store.upsert(LaunchDarkly::FEATURES, f1)
83
- expect(store.get(LaunchDarkly::FEATURES, key0)).to eq f1
123
+ it "can get all items" do
124
+ key2 = "thing2"
125
+ thing2 = {
126
+ key: key2,
127
+ name: "Thing 2",
128
+ version: 22,
129
+ deleted: false
130
+ }
131
+ with_inited_store([ thing1, thing2 ]) do |store|
132
+ expect(store.all(things_kind)).to eq ({ key1.to_sym => thing1, key2.to_sym => thing2 })
133
+ end
134
+ end
135
+
136
+ it "filters out deleted items when getting all" do
137
+ key2 = "thing2"
138
+ thing2 = {
139
+ key: key2,
140
+ name: "Thing 2",
141
+ version: 22,
142
+ deleted: true
143
+ }
144
+ with_inited_store([ thing1, thing2 ]) do |store|
145
+ expect(store.all(things_kind)).to eq ({ key1.to_sym => thing1 })
146
+ end
84
147
  end
85
148
 
86
- it "cannot update feature with same version" do
87
- f1 = new_version_plus(feature0, 0, { on: !feature0[:on] })
88
- store.upsert(LaunchDarkly::FEATURES, f1)
89
- expect(store.get(LaunchDarkly::FEATURES, key0)).to eq feature0
149
+ it "can add new item" do
150
+ with_inited_store([]) do |store|
151
+ store.upsert(things_kind, thing1)
152
+ expect(store.get(things_kind, key1)).to eq thing1
153
+ end
154
+ end
155
+
156
+ it "can update item with newer version" do
157
+ with_inited_store([ thing1 ]) do |store|
158
+ thing1_mod = new_version_plus(thing1, 1, { name: thing1[:name] + ' updated' })
159
+ store.upsert(things_kind, thing1_mod)
160
+ expect(store.get(things_kind, key1)).to eq thing1_mod
161
+ end
162
+ end
163
+
164
+ it "cannot update item with same version" do
165
+ with_inited_store([ thing1 ]) do |store|
166
+ thing1_mod = thing1.clone.merge({ name: thing1[:name] + ' updated' })
167
+ store.upsert(things_kind, thing1_mod)
168
+ expect(store.get(things_kind, key1)).to eq thing1
169
+ end
90
170
  end
91
171
 
92
172
  it "cannot update feature with older version" do
93
- f1 = new_version_plus(feature0, -1, { on: !feature0[:on] })
94
- store.upsert(LaunchDarkly::FEATURES, f1)
95
- expect(store.get(LaunchDarkly::FEATURES, key0)).to eq feature0
173
+ with_inited_store([ thing1 ]) do |store|
174
+ thing1_mod = new_version_plus(thing1, -1, { name: thing1[:name] + ' updated' })
175
+ store.upsert(things_kind, thing1_mod)
176
+ expect(store.get(things_kind, key1)).to eq thing1
177
+ end
96
178
  end
97
179
 
98
- it "can delete feature with newer version" do
99
- store.delete(LaunchDarkly::FEATURES, key0, feature0[:version] + 1)
100
- expect(store.get(LaunchDarkly::FEATURES, key0)).to be_nil
180
+ it "can delete item with newer version" do
181
+ with_inited_store([ thing1 ]) do |store|
182
+ store.delete(things_kind, key1, thing1[:version] + 1)
183
+ expect(store.get(things_kind, key1)).to be_nil
184
+ end
101
185
  end
102
186
 
103
- it "cannot delete feature with same version" do
104
- store.delete(LaunchDarkly::FEATURES, key0, feature0[:version])
105
- expect(store.get(LaunchDarkly::FEATURES, key0)).to eq feature0
187
+ it "cannot delete item with same version" do
188
+ with_inited_store([ thing1 ]) do |store|
189
+ store.delete(things_kind, key1, thing1[:version])
190
+ expect(store.get(things_kind, key1)).to eq thing1
191
+ end
106
192
  end
107
193
 
108
- it "cannot delete feature with older version" do
109
- store.delete(LaunchDarkly::FEATURES, key0, feature0[:version] - 1)
110
- expect(store.get(LaunchDarkly::FEATURES, key0)).to eq feature0
194
+ it "cannot delete item with older version" do
195
+ with_inited_store([ thing1 ]) do |store|
196
+ store.delete(things_kind, key1, thing1[:version] - 1)
197
+ expect(store.get(things_kind, key1)).to eq thing1
198
+ end
199
+ end
200
+
201
+ it "stores Unicode data correctly" do
202
+ flag = {
203
+ key: "my-fancy-flag",
204
+ name: "Tęst Feåtūre Flæg😺",
205
+ version: 1,
206
+ deleted: false
207
+ }
208
+ with_inited_store([]) do |store|
209
+ store.upsert(LaunchDarkly::FEATURES, flag)
210
+ expect(store.get(LaunchDarkly::FEATURES, flag[:key])).to eq flag
211
+ end
111
212
  end
112
213
  end