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.
- checksums.yaml +4 -4
- data/lib/ldclient-rb/config.rb +102 -56
- data/lib/ldclient-rb/context.rb +487 -0
- data/lib/ldclient-rb/evaluation_detail.rb +20 -20
- data/lib/ldclient-rb/events.rb +77 -132
- data/lib/ldclient-rb/flags_state.rb +4 -4
- data/lib/ldclient-rb/impl/big_segments.rb +17 -17
- data/lib/ldclient-rb/impl/context.rb +96 -0
- data/lib/ldclient-rb/impl/context_filter.rb +145 -0
- data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
- data/lib/ldclient-rb/impl/evaluator.rb +379 -131
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +31 -34
- data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
- data/lib/ldclient-rb/impl/event_sender.rb +6 -6
- data/lib/ldclient-rb/impl/event_summarizer.rb +12 -7
- data/lib/ldclient-rb/impl/event_types.rb +18 -30
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +7 -7
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +29 -29
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +92 -12
- data/lib/ldclient-rb/impl/model/clause.rb +45 -0
- data/lib/ldclient-rb/impl/model/feature_flag.rb +232 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +8 -121
- data/lib/ldclient-rb/impl/model/segment.rb +132 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +52 -12
- data/lib/ldclient-rb/impl/repeating_task.rb +1 -1
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
- data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
- data/lib/ldclient-rb/impl/util.rb +2 -2
- data/lib/ldclient-rb/in_memory_store.rb +2 -2
- data/lib/ldclient-rb/integrations/consul.rb +1 -1
- data/lib/ldclient-rb/integrations/dynamodb.rb +1 -1
- data/lib/ldclient-rb/integrations/file_data.rb +3 -3
- data/lib/ldclient-rb/integrations/redis.rb +4 -4
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +218 -62
- data/lib/ldclient-rb/integrations/test_data.rb +16 -12
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +9 -9
- data/lib/ldclient-rb/interfaces.rb +14 -14
- data/lib/ldclient-rb/ldclient.rb +94 -144
- data/lib/ldclient-rb/memoized_value.rb +1 -1
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
- data/lib/ldclient-rb/polling.rb +2 -2
- data/lib/ldclient-rb/reference.rb +274 -0
- data/lib/ldclient-rb/requestor.rb +5 -5
- data/lib/ldclient-rb/stream.rb +7 -8
- data/lib/ldclient-rb/util.rb +4 -19
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +2 -3
- metadata +34 -17
- data/lib/ldclient-rb/file_data_source.rb +0 -23
- data/lib/ldclient-rb/newrelic.rb +0 -17
- data/lib/ldclient-rb/redis_store.rb +0 -88
- 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
|
-
|
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
|
-
|
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(
|
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 +
|
259
|
-
SORT_KEY =>
|
258
|
+
PARTITION_KEY => @prefix + KEY_CONTEXT_DATA,
|
259
|
+
SORT_KEY => context_hash,
|
260
260
|
})
|
261
|
-
return nil
|
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
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
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
|
-
|
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)
|
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
|
-
|
180
|
-
|
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(
|
271
|
+
def get_membership(context_hash)
|
192
272
|
with_connection do |redis|
|
193
|
-
included_refs = redis.smembers(@prefix +
|
194
|
-
excluded_refs = redis.smembers(@prefix +
|
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
|