launchdarkly-server-sdk 6.4.0 → 7.0.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ldclient-rb/config.rb +102 -56
  3. data/lib/ldclient-rb/context.rb +487 -0
  4. data/lib/ldclient-rb/evaluation_detail.rb +20 -20
  5. data/lib/ldclient-rb/events.rb +77 -132
  6. data/lib/ldclient-rb/flags_state.rb +4 -4
  7. data/lib/ldclient-rb/impl/big_segments.rb +17 -17
  8. data/lib/ldclient-rb/impl/context.rb +96 -0
  9. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  10. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  11. data/lib/ldclient-rb/impl/evaluator.rb +379 -131
  12. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  13. data/lib/ldclient-rb/impl/evaluator_helpers.rb +31 -34
  14. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  15. data/lib/ldclient-rb/impl/event_sender.rb +6 -6
  16. data/lib/ldclient-rb/impl/event_summarizer.rb +12 -7
  17. data/lib/ldclient-rb/impl/event_types.rb +18 -30
  18. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +7 -7
  19. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +29 -29
  20. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
  21. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +92 -12
  22. data/lib/ldclient-rb/impl/model/clause.rb +45 -0
  23. data/lib/ldclient-rb/impl/model/feature_flag.rb +232 -0
  24. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +8 -121
  25. data/lib/ldclient-rb/impl/model/segment.rb +132 -0
  26. data/lib/ldclient-rb/impl/model/serialization.rb +52 -12
  27. data/lib/ldclient-rb/impl/repeating_task.rb +1 -1
  28. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  29. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  30. data/lib/ldclient-rb/impl/util.rb +2 -2
  31. data/lib/ldclient-rb/in_memory_store.rb +2 -2
  32. data/lib/ldclient-rb/integrations/consul.rb +1 -1
  33. data/lib/ldclient-rb/integrations/dynamodb.rb +1 -1
  34. data/lib/ldclient-rb/integrations/file_data.rb +3 -3
  35. data/lib/ldclient-rb/integrations/redis.rb +4 -4
  36. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +218 -62
  37. data/lib/ldclient-rb/integrations/test_data.rb +16 -12
  38. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +9 -9
  39. data/lib/ldclient-rb/interfaces.rb +14 -14
  40. data/lib/ldclient-rb/ldclient.rb +94 -144
  41. data/lib/ldclient-rb/memoized_value.rb +1 -1
  42. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  43. data/lib/ldclient-rb/polling.rb +2 -2
  44. data/lib/ldclient-rb/reference.rb +274 -0
  45. data/lib/ldclient-rb/requestor.rb +5 -5
  46. data/lib/ldclient-rb/stream.rb +7 -8
  47. data/lib/ldclient-rb/util.rb +4 -19
  48. data/lib/ldclient-rb/version.rb +1 -1
  49. data/lib/ldclient-rb.rb +2 -3
  50. metadata +34 -17
  51. data/lib/ldclient-rb/file_data_source.rb +0 -23
  52. data/lib/ldclient-rb/newrelic.rb +0 -17
  53. data/lib/ldclient-rb/redis_store.rb +0 -88
  54. data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -16,28 +16,28 @@ module LaunchDarkly
16
16
  AWS_SDK_ENABLED = false
17
17
  end
18
18
  end
19
-
19
+
20
20
  PARTITION_KEY = "namespace"
21
21
  SORT_KEY = "key"
22
22
 
23
23
  def initialize(table_name, opts)
24
- if !AWS_SDK_ENABLED
24
+ unless AWS_SDK_ENABLED
25
25
  raise RuntimeError.new("can't use #{description} without the aws-sdk or aws-sdk-dynamodb gem")
26
26
  end
27
-
27
+
28
28
  @table_name = table_name
29
29
  @prefix = opts[:prefix] ? (opts[:prefix] + ":") : ""
30
30
  @logger = opts[:logger] || Config.default_logger
31
-
31
+
32
32
  if !opts[:existing_client].nil?
33
33
  @client = opts[:existing_client]
34
34
  else
35
35
  @client = Aws::DynamoDB::Client.new(opts[:dynamodb_opts] || {})
36
36
  end
37
-
37
+
38
38
  @logger.info("#{description}: using DynamoDB table \"#{table_name}\"")
39
39
  end
40
-
40
+
41
41
  def stop
42
42
  # AWS client doesn't seem to have a close method
43
43
  end
@@ -46,7 +46,7 @@ module LaunchDarkly
46
46
  "DynamoDB"
47
47
  end
48
48
  end
49
-
49
+
50
50
  #
51
51
  # Internal implementation of the DynamoDB feature store, intended to be used with CachingStoreWrapper.
52
52
  #
@@ -83,7 +83,7 @@ module LaunchDarkly
83
83
  del_item = make_keys_hash(tuple[0], tuple[1])
84
84
  requests.push({ delete_request: { key: del_item } })
85
85
  end
86
-
86
+
87
87
  # Now set the special key that we check in initialized_internal?
88
88
  inited_item = make_keys_hash(inited_key, inited_key)
89
89
  requests.push({ put_request: { item: inited_item } })
@@ -123,11 +123,11 @@ module LaunchDarkly
123
123
  expression_attribute_names: {
124
124
  "#namespace" => PARTITION_KEY,
125
125
  "#key" => SORT_KEY,
126
- "#version" => VERSION_ATTRIBUTE
126
+ "#version" => VERSION_ATTRIBUTE,
127
127
  },
128
128
  expression_attribute_values: {
129
- ":version" => new_item[:version]
130
- }
129
+ ":version" => new_item[:version],
130
+ },
131
131
  })
132
132
  new_item
133
133
  rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
@@ -159,7 +159,7 @@ module LaunchDarkly
159
159
  def make_keys_hash(namespace, key)
160
160
  {
161
161
  PARTITION_KEY => namespace,
162
- SORT_KEY => key
162
+ SORT_KEY => key,
163
163
  }
164
164
  end
165
165
 
@@ -170,16 +170,16 @@ module LaunchDarkly
170
170
  key_conditions: {
171
171
  PARTITION_KEY => {
172
172
  comparison_operator: "EQ",
173
- attribute_value_list: [ namespace_for_kind(kind) ]
174
- }
175
- }
173
+ attribute_value_list: [ namespace_for_kind(kind) ],
174
+ },
175
+ },
176
176
  }
177
177
  end
178
178
 
179
179
  def get_item_by_keys(namespace, key)
180
180
  @client.get_item({
181
181
  table_name: @table_name,
182
- key: make_keys_hash(namespace, key)
182
+ key: make_keys_hash(namespace, key),
183
183
  })
184
184
  end
185
185
 
@@ -190,8 +190,8 @@ module LaunchDarkly
190
190
  projection_expression: "#namespace, #key",
191
191
  expression_attribute_names: {
192
192
  "#namespace" => PARTITION_KEY,
193
- "#key" => SORT_KEY
194
- }
193
+ "#key" => SORT_KEY,
194
+ },
195
195
  })
196
196
  while true
197
197
  resp = @client.query(req)
@@ -210,7 +210,7 @@ module LaunchDarkly
210
210
  def marshal_item(kind, item)
211
211
  make_keys_hash(namespace_for_kind(kind), item[:key]).merge({
212
212
  VERSION_ATTRIBUTE => item[:version],
213
- ITEM_JSON_ATTRIBUTE => Model.serialize(kind, item)
213
+ ITEM_JSON_ATTRIBUTE => Model.serialize(kind, item),
214
214
  })
215
215
  end
216
216
 
@@ -223,11 +223,11 @@ module LaunchDarkly
223
223
  end
224
224
 
225
225
  class DynamoDBBigSegmentStore < DynamoDBStoreImplBase
226
- KEY_METADATA = 'big_segments_metadata';
227
- KEY_USER_DATA = 'big_segments_user';
228
- ATTR_SYNC_TIME = 'synchronizedOn';
229
- ATTR_INCLUDED = 'included';
230
- ATTR_EXCLUDED = 'excluded';
226
+ KEY_METADATA = 'big_segments_metadata'
227
+ KEY_CONTEXT_DATA = 'big_segments_user'
228
+ ATTR_SYNC_TIME = 'synchronizedOn'
229
+ ATTR_INCLUDED = 'included'
230
+ ATTR_EXCLUDED = 'excluded'
231
231
 
232
232
  def initialize(table_name, opts)
233
233
  super(table_name, opts)
@@ -243,7 +243,7 @@ module LaunchDarkly
243
243
  table_name: @table_name,
244
244
  key: {
245
245
  PARTITION_KEY => key,
246
- SORT_KEY => key
246
+ SORT_KEY => key,
247
247
  }
248
248
  )
249
249
  timestamp = data.item && data.item[ATTR_SYNC_TIME] ?
@@ -251,14 +251,14 @@ module LaunchDarkly
251
251
  LaunchDarkly::Interfaces::BigSegmentStoreMetadata.new(timestamp)
252
252
  end
253
253
 
254
- def get_membership(user_hash)
254
+ def get_membership(context_hash)
255
255
  data = @client.get_item(
256
256
  table_name: @table_name,
257
257
  key: {
258
- PARTITION_KEY => @prefix + KEY_USER_DATA,
259
- SORT_KEY => user_hash
258
+ PARTITION_KEY => @prefix + KEY_CONTEXT_DATA,
259
+ SORT_KEY => context_hash,
260
260
  })
261
- return nil if !data.item
261
+ return nil unless data.item
262
262
  excluded_refs = data.item[ATTR_EXCLUDED] || []
263
263
  included_refs = data.item[ATTR_INCLUDED] || []
264
264
  if excluded_refs.empty? && included_refs.empty?
@@ -48,7 +48,7 @@ module LaunchDarkly
48
48
 
49
49
  def start
50
50
  ready = Concurrent::Event.new
51
-
51
+
52
52
  # We will return immediately regardless of whether the file load succeeded or failed -
53
53
  # the difference can be detected by checking "initialized?"
54
54
  ready.set
@@ -63,9 +63,9 @@ module LaunchDarkly
63
63
 
64
64
  ready
65
65
  end
66
-
66
+
67
67
  def stop
68
- @listener.stop if !@listener.nil?
68
+ @listener.stop unless @listener.nil?
69
69
  end
70
70
 
71
71
  private
@@ -73,7 +73,7 @@ module LaunchDarkly
73
73
  def load_all
74
74
  all_data = {
75
75
  FEATURES => {},
76
- SEGMENTS => {}
76
+ SEGMENTS => {},
77
77
  }
78
78
  @paths.each do |path|
79
79
  begin
@@ -121,12 +121,12 @@ module LaunchDarkly
121
121
 
122
122
  def add_item(all_data, kind, item)
123
123
  items = all_data[kind]
124
- raise ArgumentError, "Received unknown item kind #{kind} in add_data" if items.nil? # shouldn't be possible since we preinitialize the hash
124
+ raise ArgumentError, "Received unknown item kind #{kind[:namespace]} in add_data" if items.nil? # shouldn't be possible since we preinitialize the hash
125
125
  key = item[:key].to_sym
126
- if !items[key].nil?
126
+ unless items[key].nil?
127
127
  raise ArgumentError, "#{kind[:namespace]} key \"#{item[:key]}\" was used more than once"
128
128
  end
129
- items[key] = item
129
+ items[key] = Model.deserialize(kind, item)
130
130
  end
131
131
 
132
132
  def make_flag_with_value(key, value)
@@ -134,7 +134,7 @@ module LaunchDarkly
134
134
  key: key,
135
135
  on: true,
136
136
  fallthrough: { variation: 0 },
137
- variations: [ value ]
137
+ variations: [ value ],
138
138
  }
139
139
  end
140
140
 
@@ -5,6 +5,87 @@ module LaunchDarkly
5
5
  module Impl
6
6
  module Integrations
7
7
  module Redis
8
+ #
9
+ # An implementation of the LaunchDarkly client's feature store that uses a Redis
10
+ # instance. This object holds feature flags and related data received from the
11
+ # streaming API. Feature data can also be further cached in memory to reduce overhead
12
+ # of calls to Redis.
13
+ #
14
+ # To use this class, you must first have the `redis` and `connection-pool` gems
15
+ # installed. Then, create an instance and store it in the `feature_store` property
16
+ # of your client configuration.
17
+ #
18
+ class RedisFeatureStore
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.
25
+
26
+ #
27
+ # Constructor for a RedisFeatureStore instance.
28
+ #
29
+ # @param opts [Hash] the configuration options
30
+ # @option opts [String] :redis_url URL of the Redis instance (shortcut for omitting redis_opts)
31
+ # @option opts [Hash] :redis_opts options to pass to the Redis constructor (if you want to specify more than just redis_url)
32
+ # @option opts [String] :prefix namespace prefix to add to all hash keys used by LaunchDarkly
33
+ # @option opts [Logger] :logger a `Logger` instance; defaults to `Config.default_logger`
34
+ # @option opts [Integer] :max_connections size of the Redis connection pool
35
+ # @option opts [Integer] :expiration expiration time for the in-memory cache, in seconds; 0 for no local caching
36
+ # @option opts [Integer] :capacity maximum number of feature flags (or related objects) to cache locally
37
+ # @option opts [Object] :pool custom connection pool, if desired
38
+ # @option opts [Boolean] :pool_shutdown_on_close whether calling `close` should shutdown the custom connection pool.
39
+ #
40
+ def initialize(opts = {})
41
+ core = RedisFeatureStoreCore.new(opts)
42
+ @wrapper = LaunchDarkly::Integrations::Util::CachingStoreWrapper.new(core, opts)
43
+ end
44
+
45
+ #
46
+ # Default value for the `redis_url` constructor parameter; points to an instance of Redis
47
+ # running at `localhost` with its default port.
48
+ #
49
+ def self.default_redis_url
50
+ LaunchDarkly::Integrations::Redis::default_redis_url
51
+ end
52
+
53
+ #
54
+ # Default value for the `prefix` constructor parameter.
55
+ #
56
+ def self.default_prefix
57
+ LaunchDarkly::Integrations::Redis::default_prefix
58
+ end
59
+
60
+ def get(kind, key)
61
+ @wrapper.get(kind, key)
62
+ end
63
+
64
+ def all(kind)
65
+ @wrapper.all(kind)
66
+ end
67
+
68
+ def delete(kind, key, version)
69
+ @wrapper.delete(kind, key, version)
70
+ end
71
+
72
+ def init(all_data)
73
+ @wrapper.init(all_data)
74
+ end
75
+
76
+ def upsert(kind, item)
77
+ @wrapper.upsert(kind, item)
78
+ end
79
+
80
+ def initialized?
81
+ @wrapper.initialized?
82
+ end
83
+
84
+ def stop
85
+ @wrapper.stop
86
+ end
87
+ end
88
+
8
89
  class RedisStoreImplBase
9
90
  begin
10
91
  require "redis"
@@ -15,7 +96,7 @@ module LaunchDarkly
15
96
  end
16
97
 
17
98
  def initialize(opts)
18
- if !REDIS_ENABLED
99
+ unless REDIS_ENABLED
19
100
  raise RuntimeError.new("can't use #{description} because one of these gems is missing: redis, connection_pool")
20
101
  end
21
102
 
@@ -28,7 +109,7 @@ module LaunchDarkly
28
109
  @logger = opts[:logger] || Config.default_logger
29
110
  @test_hook = opts[:test_hook] # used for unit tests, deliberately undocumented
30
111
 
31
- @stopped = Concurrent::AtomicBoolean.new(false)
112
+ @stopped = Concurrent::AtomicBoolean.new()
32
113
 
33
114
  with_connection do |redis|
34
115
  @logger.info("#{description}: using Redis instance at #{redis.connection[:host]}:#{redis.connection[:port]} and prefix: #{@prefix}")
@@ -55,13 +136,11 @@ module LaunchDarkly
55
136
  if opts[:redis_url]
56
137
  redis_opts[:url] = opts[:redis_url]
57
138
  end
58
- if !redis_opts.include?(:url)
139
+ unless redis_opts.include?(:url)
59
140
  redis_opts[:url] = LaunchDarkly::Integrations::Redis::default_redis_url
60
141
  end
61
142
  max_connections = opts[:max_connections] || 16
62
- return opts[:pool] || ConnectionPool.new(size: max_connections) do
63
- ::Redis.new(redis_opts)
64
- end
143
+ opts[:pool] || ConnectionPool.new(size: max_connections) { ::Redis.new(redis_opts) }
65
144
  end
66
145
  end
67
146
 
@@ -135,6 +214,7 @@ module LaunchDarkly
135
214
  else
136
215
  final_item = old_item
137
216
  action = new_item[:deleted] ? "delete" : "update"
217
+ # rubocop:disable Layout/LineLength
138
218
  @logger.warn { "RedisFeatureStore: attempted to #{action} #{key} version: #{old_item[:version]} in '#{kind[:namespace]}' with a version that is the same or older: #{new_item[:version]}" }
139
219
  end
140
220
  redis.unwatch
@@ -151,7 +231,7 @@ module LaunchDarkly
151
231
  private
152
232
 
153
233
  def before_update_transaction(base_key, key)
154
- @test_hook.before_update_transaction(base_key, key) if !@test_hook.nil?
234
+ @test_hook.before_update_transaction(base_key, key) unless @test_hook.nil?
155
235
  end
156
236
 
157
237
  def items_key(kind)
@@ -176,8 +256,8 @@ module LaunchDarkly
176
256
  #
177
257
  class RedisBigSegmentStore < RedisStoreImplBase
178
258
  KEY_LAST_UP_TO_DATE = ':big_segments_synchronized_on'
179
- KEY_USER_INCLUDE = ':big_segment_include:'
180
- KEY_USER_EXCLUDE = ':big_segment_exclude:'
259
+ KEY_CONTEXT_INCLUDE = ':big_segment_include:'
260
+ KEY_CONTEXT_EXCLUDE = ':big_segment_exclude:'
181
261
 
182
262
  def description
183
263
  "RedisBigSegmentStore"
@@ -188,10 +268,10 @@ module LaunchDarkly
188
268
  Interfaces::BigSegmentStoreMetadata.new(value.nil? ? nil : value.to_i)
189
269
  end
190
270
 
191
- def get_membership(user_hash)
271
+ def get_membership(context_hash)
192
272
  with_connection do |redis|
193
- included_refs = redis.smembers(@prefix + KEY_USER_INCLUDE + user_hash)
194
- excluded_refs = redis.smembers(@prefix + KEY_USER_EXCLUDE + user_hash)
273
+ included_refs = redis.smembers(@prefix + KEY_CONTEXT_INCLUDE + context_hash)
274
+ excluded_refs = redis.smembers(@prefix + KEY_CONTEXT_EXCLUDE + context_hash)
195
275
  if !included_refs && !excluded_refs
196
276
  nil
197
277
  else
@@ -0,0 +1,45 @@
1
+ require "ldclient-rb/reference"
2
+
3
+
4
+ # See serialization.rb for implementation notes on the data model classes.
5
+
6
+ module LaunchDarkly
7
+ module Impl
8
+ module Model
9
+ class Clause
10
+ def initialize(data, errors_out = nil)
11
+ @data = data
12
+ @context_kind = data[:contextKind]
13
+ @op = data[:op].to_sym
14
+ if @op == :segmentMatch
15
+ @attribute = nil
16
+ else
17
+ @attribute = (@context_kind.nil? || @context_kind.empty?) ? Reference.create_literal(data[:attribute]) : Reference.create(data[:attribute])
18
+ unless errors_out.nil? || @attribute.error.nil?
19
+ errors_out << "clause has invalid attribute: #{@attribute.error}"
20
+ end
21
+ end
22
+ @values = data[:values] || []
23
+ @negate = !!data[:negate]
24
+ end
25
+
26
+ # @return [Hash]
27
+ attr_reader :data
28
+ # @return [String|nil]
29
+ attr_reader :context_kind
30
+ # @return [LaunchDarkly::Reference]
31
+ attr_reader :attribute
32
+ # @return [Symbol]
33
+ attr_reader :op
34
+ # @return [Array]
35
+ attr_reader :values
36
+ # @return [Boolean]
37
+ attr_reader :negate
38
+
39
+ def as_json
40
+ @data
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,232 @@
1
+ require "ldclient-rb/impl/evaluator_helpers"
2
+ require "ldclient-rb/impl/model/clause"
3
+ require "set"
4
+
5
+ # See serialization.rb for implementation notes on the data model classes.
6
+
7
+ def check_variation_range(flag, errors_out, variation, description)
8
+ unless flag.nil? || errors_out.nil? || variation.nil?
9
+ if variation < 0 || variation >= flag.variations.length
10
+ errors_out << "#{description} has invalid variation index"
11
+ end
12
+ end
13
+ end
14
+
15
+ module LaunchDarkly
16
+ module Impl
17
+ module Model
18
+ class FeatureFlag
19
+ # @param data [Hash]
20
+ # @param logger [Logger|nil]
21
+ def initialize(data, logger = nil)
22
+ raise ArgumentError, "expected hash but got #{data.class}" unless data.is_a?(Hash)
23
+ errors = []
24
+ @data = data
25
+ @key = data[:key]
26
+ @version = data[:version]
27
+ @deleted = !!data[:deleted]
28
+ return if @deleted
29
+ @variations = data[:variations] || []
30
+ @on = !!data[:on]
31
+ fallthrough = data[:fallthrough] || {}
32
+ @fallthrough = VariationOrRollout.new(fallthrough[:variation], fallthrough[:rollout], self, errors, "fallthrough")
33
+ @off_variation = data[:offVariation]
34
+ check_variation_range(self, errors, @off_variation, "off variation")
35
+ @prerequisites = (data[:prerequisites] || []).map do |prereq_data|
36
+ Prerequisite.new(prereq_data, self, errors)
37
+ end
38
+ @targets = (data[:targets] || []).map do |target_data|
39
+ Target.new(target_data, self, errors)
40
+ end
41
+ @context_targets = (data[:contextTargets] || []).map do |target_data|
42
+ Target.new(target_data, self, errors)
43
+ end
44
+ @rules = (data[:rules] || []).map.with_index do |rule_data, index|
45
+ FlagRule.new(rule_data, index, self, errors)
46
+ end
47
+ @salt = data[:salt]
48
+ @off_result = EvaluatorHelpers.evaluation_detail_for_off_variation(self, EvaluationReason::off)
49
+ @fallthrough_results = Preprocessor.precompute_multi_variation_results(self,
50
+ EvaluationReason::fallthrough(false), EvaluationReason::fallthrough(true))
51
+ unless logger.nil?
52
+ errors.each do |message|
53
+ logger.error("[LDClient] Data inconsistency in feature flag \"#{@key}\": #{message}")
54
+ end
55
+ end
56
+ end
57
+
58
+ # @return [Hash]
59
+ attr_reader :data
60
+ # @return [String]
61
+ attr_reader :key
62
+ # @return [Integer]
63
+ attr_reader :version
64
+ # @return [Boolean]
65
+ attr_reader :deleted
66
+ # @return [Array]
67
+ attr_reader :variations
68
+ # @return [Boolean]
69
+ attr_reader :on
70
+ # @return [Integer|nil]
71
+ attr_reader :off_variation
72
+ # @return [LaunchDarkly::Impl::Model::VariationOrRollout]
73
+ attr_reader :fallthrough
74
+ # @return [LaunchDarkly::EvaluationDetail]
75
+ attr_reader :off_result
76
+ # @return [LaunchDarkly::Impl::Model::EvalResultFactoryMultiVariations]
77
+ attr_reader :fallthrough_results
78
+ # @return [Array<LaunchDarkly::Impl::Model::Prerequisite>]
79
+ attr_reader :prerequisites
80
+ # @return [Array<LaunchDarkly::Impl::Model::Target>]
81
+ attr_reader :targets
82
+ # @return [Array<LaunchDarkly::Impl::Model::Target>]
83
+ attr_reader :context_targets
84
+ # @return [Array<LaunchDarkly::Impl::Model::FlagRule>]
85
+ attr_reader :rules
86
+ # @return [String]
87
+ attr_reader :salt
88
+
89
+ # This method allows us to read properties of the object as if it's just a hash. Currently this is
90
+ # necessary because some data store logic is still written to expect hashes; we can remove it once
91
+ # we migrate entirely to using attributes of the class.
92
+ def [](key)
93
+ @data[key]
94
+ end
95
+
96
+ def ==(other)
97
+ other.is_a?(FeatureFlag) && other.data == self.data
98
+ end
99
+
100
+ def as_json(*) # parameter is unused, but may be passed if we're using the json gem
101
+ @data
102
+ end
103
+
104
+ # Same as as_json, but converts the JSON structure into a string.
105
+ def to_json(*a)
106
+ as_json.to_json(a)
107
+ end
108
+ end
109
+
110
+ class Prerequisite
111
+ def initialize(data, flag, errors_out = nil)
112
+ @data = data
113
+ @key = data[:key]
114
+ @variation = data[:variation]
115
+ @failure_result = EvaluatorHelpers.evaluation_detail_for_off_variation(flag,
116
+ EvaluationReason::prerequisite_failed(@key))
117
+ check_variation_range(flag, errors_out, @variation, "prerequisite")
118
+ end
119
+
120
+ # @return [Hash]
121
+ attr_reader :data
122
+ # @return [String]
123
+ attr_reader :key
124
+ # @return [Integer]
125
+ attr_reader :variation
126
+ # @return [LaunchDarkly::EvaluationDetail]
127
+ attr_reader :failure_result
128
+ end
129
+
130
+ class Target
131
+ def initialize(data, flag, errors_out = nil)
132
+ @kind = data[:contextKind] || LDContext::KIND_DEFAULT
133
+ @data = data
134
+ @values = Set.new(data[:values] || [])
135
+ @variation = data[:variation]
136
+ @match_result = EvaluatorHelpers.evaluation_detail_for_variation(flag,
137
+ data[:variation], EvaluationReason::target_match)
138
+ check_variation_range(flag, errors_out, @variation, "target")
139
+ end
140
+
141
+ # @return [String]
142
+ attr_reader :kind
143
+ # @return [Hash]
144
+ attr_reader :data
145
+ # @return [Set]
146
+ attr_reader :values
147
+ # @return [Integer]
148
+ attr_reader :variation
149
+ # @return [LaunchDarkly::EvaluationDetail]
150
+ attr_reader :match_result
151
+ end
152
+
153
+ class FlagRule
154
+ def initialize(data, rule_index, flag, errors_out = nil)
155
+ @data = data
156
+ @clauses = (data[:clauses] || []).map do |clause_data|
157
+ Clause.new(clause_data, errors_out)
158
+ end
159
+ @variation_or_rollout = VariationOrRollout.new(data[:variation], data[:rollout], flag, errors_out, 'rule')
160
+ rule_id = data[:id]
161
+ match_reason = EvaluationReason::rule_match(rule_index, rule_id)
162
+ match_reason_in_experiment = EvaluationReason::rule_match(rule_index, rule_id, true)
163
+ @match_results = Preprocessor.precompute_multi_variation_results(flag, match_reason, match_reason_in_experiment)
164
+ end
165
+
166
+ # @return [Hash]
167
+ attr_reader :data
168
+ # @return [Array<LaunchDarkly::Impl::Model::Clause>]
169
+ attr_reader :clauses
170
+ # @return [LaunchDarkly::Impl::Model::EvalResultFactoryMultiVariations]
171
+ attr_reader :match_results
172
+ # @return [LaunchDarkly::Impl::Model::VariationOrRollout]
173
+ attr_reader :variation_or_rollout
174
+ end
175
+
176
+ class VariationOrRollout
177
+ def initialize(variation, rollout_data, flag = nil, errors_out = nil, description = nil)
178
+ @variation = variation
179
+ check_variation_range(flag, errors_out, variation, description)
180
+ @rollout = rollout_data.nil? ? nil : Rollout.new(rollout_data, flag, errors_out, description)
181
+ end
182
+
183
+ # @return [Integer|nil]
184
+ attr_reader :variation
185
+ # @return [Rollout|nil] currently we do not have a model class for the rollout
186
+ attr_reader :rollout
187
+ end
188
+
189
+ class Rollout
190
+ def initialize(data, flag = nil, errors_out = nil, description = nil)
191
+ @context_kind = data[:contextKind]
192
+ @variations = (data[:variations] || []).map { |v| WeightedVariation.new(v, flag, errors_out, description) }
193
+ @bucket_by = data[:bucketBy]
194
+ @kind = data[:kind]
195
+ @is_experiment = @kind == "experiment"
196
+ @seed = data[:seed]
197
+ end
198
+
199
+ # @return [String|nil]
200
+ attr_reader :context_kind
201
+ # @return [Array<WeightedVariation>]
202
+ attr_reader :variations
203
+ # @return [String|nil]
204
+ attr_reader :bucket_by
205
+ # @return [String|nil]
206
+ attr_reader :kind
207
+ # @return [Boolean]
208
+ attr_reader :is_experiment
209
+ # @return [Integer|nil]
210
+ attr_reader :seed
211
+ end
212
+
213
+ class WeightedVariation
214
+ def initialize(data, flag = nil, errors_out = nil, description = nil)
215
+ @variation = data[:variation]
216
+ @weight = data[:weight]
217
+ @untracked = !!data[:untracked]
218
+ check_variation_range(flag, errors_out, @variation, description)
219
+ end
220
+
221
+ # @return [Integer]
222
+ attr_reader :variation
223
+ # @return [Integer]
224
+ attr_reader :weight
225
+ # @return [Boolean]
226
+ attr_reader :untracked
227
+ end
228
+
229
+ # Clause is defined in its own file because clauses are used by both flags and segments
230
+ end
231
+ end
232
+ end