launchdarkly-server-sdk 6.1.1 → 6.4.0
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/README.md +4 -5
- data/lib/ldclient-rb/config.rb +118 -4
- data/lib/ldclient-rb/evaluation_detail.rb +104 -14
- data/lib/ldclient-rb/events.rb +201 -107
- data/lib/ldclient-rb/file_data_source.rb +9 -300
- data/lib/ldclient-rb/flags_state.rb +23 -12
- data/lib/ldclient-rb/impl/big_segments.rb +117 -0
- data/lib/ldclient-rb/impl/diagnostic_events.rb +1 -1
- data/lib/ldclient-rb/impl/evaluator.rb +116 -62
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +22 -9
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +53 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +1 -1
- data/lib/ldclient-rb/impl/event_summarizer.rb +63 -0
- data/lib/ldclient-rb/impl/event_types.rb +90 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +82 -18
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +212 -0
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +84 -31
- data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +177 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +7 -37
- data/lib/ldclient-rb/impl/repeating_task.rb +47 -0
- data/lib/ldclient-rb/impl/util.rb +62 -1
- data/lib/ldclient-rb/integrations/consul.rb +8 -1
- data/lib/ldclient-rb/integrations/dynamodb.rb +48 -3
- data/lib/ldclient-rb/integrations/file_data.rb +108 -0
- data/lib/ldclient-rb/integrations/redis.rb +42 -2
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +438 -0
- data/lib/ldclient-rb/integrations/test_data.rb +209 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +5 -0
- data/lib/ldclient-rb/integrations.rb +2 -51
- data/lib/ldclient-rb/interfaces.rb +152 -2
- data/lib/ldclient-rb/ldclient.rb +131 -33
- data/lib/ldclient-rb/polling.rb +22 -41
- data/lib/ldclient-rb/requestor.rb +3 -3
- data/lib/ldclient-rb/stream.rb +4 -3
- data/lib/ldclient-rb/util.rb +10 -1
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +0 -1
- metadata +35 -132
- data/.circleci/config.yml +0 -40
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -37
- data/.github/ISSUE_TEMPLATE/config.yml +0 -5
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- data/.github/pull_request_template.md +0 -21
- data/.gitignore +0 -16
- data/.hound.yml +0 -2
- data/.ldrelease/build-docs.sh +0 -18
- data/.ldrelease/circleci/linux/execute.sh +0 -18
- data/.ldrelease/circleci/mac/execute.sh +0 -18
- data/.ldrelease/circleci/template/build.sh +0 -29
- data/.ldrelease/circleci/template/publish.sh +0 -23
- data/.ldrelease/circleci/template/set-gem-home.sh +0 -7
- data/.ldrelease/circleci/template/test.sh +0 -10
- data/.ldrelease/circleci/template/update-version.sh +0 -8
- data/.ldrelease/circleci/windows/execute.ps1 +0 -19
- data/.ldrelease/config.yml +0 -29
- data/.rspec +0 -2
- data/.rubocop.yml +0 -600
- data/.simplecov +0 -4
- data/CHANGELOG.md +0 -351
- data/CODEOWNERS +0 -1
- data/CONTRIBUTING.md +0 -37
- data/Gemfile +0 -3
- data/azure-pipelines.yml +0 -51
- data/docs/Makefile +0 -26
- data/docs/index.md +0 -9
- data/launchdarkly-server-sdk.gemspec +0 -45
- data/lib/ldclient-rb/event_summarizer.rb +0 -55
- data/lib/ldclient-rb/impl/event_factory.rb +0 -120
- data/spec/config_spec.rb +0 -63
- data/spec/diagnostic_events_spec.rb +0 -163
- data/spec/evaluation_detail_spec.rb +0 -135
- data/spec/event_sender_spec.rb +0 -197
- data/spec/event_summarizer_spec.rb +0 -63
- data/spec/events_spec.rb +0 -607
- data/spec/expiring_cache_spec.rb +0 -76
- data/spec/feature_store_spec_base.rb +0 -213
- data/spec/file_data_source_spec.rb +0 -283
- data/spec/fixtures/feature.json +0 -37
- data/spec/fixtures/feature1.json +0 -36
- data/spec/fixtures/user.json +0 -9
- data/spec/flags_state_spec.rb +0 -81
- data/spec/http_util.rb +0 -132
- data/spec/impl/evaluator_bucketing_spec.rb +0 -111
- data/spec/impl/evaluator_clause_spec.rb +0 -55
- data/spec/impl/evaluator_operators_spec.rb +0 -141
- data/spec/impl/evaluator_rule_spec.rb +0 -96
- data/spec/impl/evaluator_segment_spec.rb +0 -125
- data/spec/impl/evaluator_spec.rb +0 -305
- data/spec/impl/evaluator_spec_base.rb +0 -75
- data/spec/impl/model/serialization_spec.rb +0 -41
- data/spec/in_memory_feature_store_spec.rb +0 -12
- data/spec/integrations/consul_feature_store_spec.rb +0 -40
- data/spec/integrations/dynamodb_feature_store_spec.rb +0 -103
- data/spec/integrations/store_wrapper_spec.rb +0 -276
- data/spec/launchdarkly-server-sdk_spec.rb +0 -13
- data/spec/launchdarkly-server-sdk_spec_autoloadtest.rb +0 -9
- data/spec/ldclient_end_to_end_spec.rb +0 -157
- data/spec/ldclient_spec.rb +0 -643
- data/spec/newrelic_spec.rb +0 -5
- data/spec/polling_spec.rb +0 -120
- data/spec/redis_feature_store_spec.rb +0 -121
- data/spec/requestor_spec.rb +0 -196
- data/spec/segment_store_spec_base.rb +0 -95
- data/spec/simple_lru_cache_spec.rb +0 -24
- data/spec/spec_helper.rb +0 -9
- data/spec/store_spec.rb +0 -10
- data/spec/stream_spec.rb +0 -45
- data/spec/user_filter_spec.rb +0 -91
- data/spec/util_spec.rb +0 -17
- data/spec/version_spec.rb +0 -7
@@ -0,0 +1,438 @@
|
|
1
|
+
require 'ldclient-rb/util'
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
module Integrations
|
5
|
+
class TestData
|
6
|
+
#
|
7
|
+
# A builder for feature flag configurations to be used with {TestData}.
|
8
|
+
#
|
9
|
+
# @see TestData#flag
|
10
|
+
# @see TestData#update
|
11
|
+
#
|
12
|
+
class FlagBuilder
|
13
|
+
attr_reader :key
|
14
|
+
|
15
|
+
# @private
|
16
|
+
def initialize(key)
|
17
|
+
@key = key
|
18
|
+
@on = true
|
19
|
+
@variations = []
|
20
|
+
end
|
21
|
+
|
22
|
+
# @private
|
23
|
+
def initialize_copy(other)
|
24
|
+
super(other)
|
25
|
+
@variations = @variations.clone
|
26
|
+
@rules = @rules.nil? ? nil : deep_copy_array(@rules)
|
27
|
+
@targets = @targets.nil? ? nil : deep_copy_hash(@targets)
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Sets targeting to be on or off for this flag.
|
32
|
+
#
|
33
|
+
# The effect of this depends on the rest of the flag configuration, just as it does on the
|
34
|
+
# real LaunchDarkly dashboard. In the default configuration that you get from calling
|
35
|
+
# {TestData#flag} with a new flag key, the flag will return `false`
|
36
|
+
# whenever targeting is off, and `true` when targeting is on.
|
37
|
+
#
|
38
|
+
# @param on [Boolean] true if targeting should be on
|
39
|
+
# @return [FlagBuilder] the builder
|
40
|
+
#
|
41
|
+
def on(on)
|
42
|
+
@on = on
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# Specifies the fallthrough variation. The fallthrough is the value
|
48
|
+
# that is returned if targeting is on and the user was not matched by a more specific
|
49
|
+
# target or rule.
|
50
|
+
#
|
51
|
+
# If the flag was previously configured with other variations and the variation specified is a boolean,
|
52
|
+
# this also changes it to a boolean flag.
|
53
|
+
#
|
54
|
+
# @param variation [Boolean, Integer] true or false or the desired fallthrough variation index:
|
55
|
+
# 0 for the first, 1 for the second, etc.
|
56
|
+
# @return [FlagBuilder] the builder
|
57
|
+
#
|
58
|
+
def fallthrough_variation(variation)
|
59
|
+
if LaunchDarkly::Impl::Util.is_bool variation then
|
60
|
+
boolean_flag.fallthrough_variation(variation_for_boolean(variation))
|
61
|
+
else
|
62
|
+
@fallthrough_variation = variation
|
63
|
+
self
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Specifies the off variation for a flag. This is the variation that is returned
|
69
|
+
# whenever targeting is off.
|
70
|
+
#
|
71
|
+
# If the flag was previously configured with other variations and the variation specified is a boolean,
|
72
|
+
# this also changes it to a boolean flag.
|
73
|
+
#
|
74
|
+
# @param variation [Boolean, Integer] true or false or the desired off variation index:
|
75
|
+
# 0 for the first, 1 for the second, etc.
|
76
|
+
# @return [FlagBuilder] the builder
|
77
|
+
#
|
78
|
+
def off_variation(variation)
|
79
|
+
if LaunchDarkly::Impl::Util.is_bool variation then
|
80
|
+
boolean_flag.off_variation(variation_for_boolean(variation))
|
81
|
+
else
|
82
|
+
@off_variation = variation
|
83
|
+
self
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Changes the allowable variation values for the flag.
|
89
|
+
#
|
90
|
+
# The value may be of any valid JSON type. For instance, a boolean flag
|
91
|
+
# normally has `true, false`; a string-valued flag might have
|
92
|
+
# `'red', 'green'`; etc.
|
93
|
+
#
|
94
|
+
# @example A single variation
|
95
|
+
# td.flag('new-flag')
|
96
|
+
# .variations(true)
|
97
|
+
#
|
98
|
+
# @example Multiple variations
|
99
|
+
# td.flag('new-flag')
|
100
|
+
# .variations('red', 'green', 'blue')
|
101
|
+
#
|
102
|
+
# @param variations [Array<Object>] the the desired variations
|
103
|
+
# @return [FlagBuilder] the builder
|
104
|
+
#
|
105
|
+
def variations(*variations)
|
106
|
+
@variations = variations
|
107
|
+
self
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# Sets the flag to always return the specified variation for all users.
|
112
|
+
#
|
113
|
+
# The variation is specified, Targeting is switched on, and any existing targets or rules are removed.
|
114
|
+
# The fallthrough variation is set to the specified value. The off variation is left unchanged.
|
115
|
+
#
|
116
|
+
# If the flag was previously configured with other variations and the variation specified is a boolean,
|
117
|
+
# this also changes it to a boolean flag.
|
118
|
+
#
|
119
|
+
# @param variation [Boolean, Integer] true or false or the desired variation index to return:
|
120
|
+
# 0 for the first, 1 for the second, etc.
|
121
|
+
# @return [FlagBuilder] the builder
|
122
|
+
#
|
123
|
+
def variation_for_all_users(variation)
|
124
|
+
if LaunchDarkly::Impl::Util.is_bool variation then
|
125
|
+
boolean_flag.variation_for_all_users(variation_for_boolean(variation))
|
126
|
+
else
|
127
|
+
on(true).clear_rules.clear_user_targets.fallthrough_variation(variation)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
#
|
132
|
+
# Sets the flag to always return the specified variation value for all users.
|
133
|
+
#
|
134
|
+
# The value may be of any valid JSON type. This method changes the
|
135
|
+
# flag to have only a single variation, which is this value, and to return the same
|
136
|
+
# variation regardless of whether targeting is on or off. Any existing targets or rules
|
137
|
+
# are removed.
|
138
|
+
#
|
139
|
+
# @param value [Object] the desired value to be returned for all users
|
140
|
+
# @return [FlagBuilder] the builder
|
141
|
+
#
|
142
|
+
def value_for_all_users(value)
|
143
|
+
variations(value).variation_for_all_users(0)
|
144
|
+
end
|
145
|
+
|
146
|
+
#
|
147
|
+
# Sets the flag to return the specified variation for a specific user key when targeting
|
148
|
+
# is on.
|
149
|
+
#
|
150
|
+
# This has no effect when targeting is turned off for the flag.
|
151
|
+
#
|
152
|
+
# If the flag was previously configured with other variations and the variation specified is a boolean,
|
153
|
+
# this also changes it to a boolean flag.
|
154
|
+
#
|
155
|
+
# @param user_key [String] a user key
|
156
|
+
# @param variation [Boolean, Integer] true or false or the desired variation index to return:
|
157
|
+
# 0 for the first, 1 for the second, etc.
|
158
|
+
# @return [FlagBuilder] the builder
|
159
|
+
#
|
160
|
+
def variation_for_user(user_key, variation)
|
161
|
+
if LaunchDarkly::Impl::Util.is_bool variation then
|
162
|
+
boolean_flag.variation_for_user(user_key, variation_for_boolean(variation))
|
163
|
+
else
|
164
|
+
if @targets.nil? then
|
165
|
+
@targets = Hash.new
|
166
|
+
end
|
167
|
+
@variations.count.times do | i |
|
168
|
+
if i == variation then
|
169
|
+
if @targets[i].nil? then
|
170
|
+
@targets[i] = [user_key]
|
171
|
+
else
|
172
|
+
@targets[i].push(user_key)
|
173
|
+
end
|
174
|
+
elsif not @targets[i].nil? then
|
175
|
+
@targets[i].delete(user_key)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
self
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
#
|
183
|
+
# Starts defining a flag rule, using the "is one of" operator.
|
184
|
+
#
|
185
|
+
# @example create a rule that returns `true` if the name is "Patsy" or "Edina"
|
186
|
+
# testData.flag("flag")
|
187
|
+
# .if_match(:name, 'Patsy', 'Edina')
|
188
|
+
# .then_return(true);
|
189
|
+
#
|
190
|
+
# @param attribute [Symbol] the user attribute to match against
|
191
|
+
# @param values [Array<Object>] values to compare to
|
192
|
+
# @return [FlagRuleBuilder] a flag rule builder
|
193
|
+
#
|
194
|
+
# @see FlagRuleBuilder#then_return
|
195
|
+
# @see FlagRuleBuilder#and_match
|
196
|
+
# @see FlagRuleBuilder#and_not_match
|
197
|
+
#
|
198
|
+
def if_match(attribute, *values)
|
199
|
+
FlagRuleBuilder.new(self).and_match(attribute, *values)
|
200
|
+
end
|
201
|
+
|
202
|
+
#
|
203
|
+
# Starts defining a flag rule, using the "is not one of" operator.
|
204
|
+
#
|
205
|
+
# @example create a rule that returns `true` if the name is neither "Saffron" nor "Bubble"
|
206
|
+
# testData.flag("flag")
|
207
|
+
# .if_not_match(:name, 'Saffron', 'Bubble')
|
208
|
+
# .then_return(true)
|
209
|
+
#
|
210
|
+
# @param attribute [Symbol] the user attribute to match against
|
211
|
+
# @param values [Array<Object>] values to compare to
|
212
|
+
# @return [FlagRuleBuilder] a flag rule builder
|
213
|
+
#
|
214
|
+
# @see FlagRuleBuilder#then_return
|
215
|
+
# @see FlagRuleBuilder#and_match
|
216
|
+
# @see FlagRuleBuilder#and_not_match
|
217
|
+
#
|
218
|
+
def if_not_match(attribute, *values)
|
219
|
+
FlagRuleBuilder.new(self).and_not_match(attribute, *values)
|
220
|
+
end
|
221
|
+
|
222
|
+
#
|
223
|
+
# Removes any existing user targets from the flag.
|
224
|
+
# This undoes the effect of methods like {#variation_for_user}
|
225
|
+
#
|
226
|
+
# @return [FlagBuilder] the same builder
|
227
|
+
#
|
228
|
+
def clear_user_targets
|
229
|
+
@targets = nil
|
230
|
+
self
|
231
|
+
end
|
232
|
+
|
233
|
+
#
|
234
|
+
# Removes any existing rules from the flag.
|
235
|
+
# This undoes the effect of methods like {#if_match}
|
236
|
+
#
|
237
|
+
# @return [FlagBuilder] the same builder
|
238
|
+
#
|
239
|
+
def clear_rules
|
240
|
+
@rules = nil
|
241
|
+
self
|
242
|
+
end
|
243
|
+
|
244
|
+
# @private
|
245
|
+
def add_rule(rule)
|
246
|
+
if @rules.nil? then
|
247
|
+
@rules = Array.new
|
248
|
+
end
|
249
|
+
@rules.push(rule)
|
250
|
+
self
|
251
|
+
end
|
252
|
+
|
253
|
+
#
|
254
|
+
# A shortcut for setting the flag to use the standard boolean configuration.
|
255
|
+
#
|
256
|
+
# This is the default for all new flags created with {TestData#flag}.
|
257
|
+
# The flag will have two variations, `true` and `false` (in that order);
|
258
|
+
# it will return `false` whenever targeting is off, and `true` when targeting is on
|
259
|
+
# if no other settings specify otherwise.
|
260
|
+
#
|
261
|
+
# @return [FlagBuilder] the builder
|
262
|
+
#
|
263
|
+
def boolean_flag
|
264
|
+
if is_boolean_flag then
|
265
|
+
self
|
266
|
+
else
|
267
|
+
variations(true, false)
|
268
|
+
.fallthrough_variation(TRUE_VARIATION_INDEX)
|
269
|
+
.off_variation(FALSE_VARIATION_INDEX)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# @private
|
274
|
+
def build(version)
|
275
|
+
res = { key: @key,
|
276
|
+
version: version,
|
277
|
+
on: @on,
|
278
|
+
variations: @variations,
|
279
|
+
}
|
280
|
+
|
281
|
+
unless @off_variation.nil? then
|
282
|
+
res[:offVariation] = @off_variation
|
283
|
+
end
|
284
|
+
|
285
|
+
unless @fallthrough_variation.nil? then
|
286
|
+
res[:fallthrough] = { variation: @fallthrough_variation }
|
287
|
+
end
|
288
|
+
|
289
|
+
unless @targets.nil? then
|
290
|
+
res[:targets] = @targets.collect do | variation, values |
|
291
|
+
{ variation: variation, values: values }
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
unless @rules.nil? then
|
296
|
+
res[:rules] = @rules.each_with_index.collect { | rule, i | rule.build(i) }
|
297
|
+
end
|
298
|
+
|
299
|
+
res
|
300
|
+
end
|
301
|
+
|
302
|
+
#
|
303
|
+
# A builder for feature flag rules to be used with {FlagBuilder}.
|
304
|
+
#
|
305
|
+
# In the LaunchDarkly model, a flag can have any number of rules, and a rule can have any number of
|
306
|
+
# clauses. A clause is an individual test such as "name is 'X'". A rule matches a user if all of the
|
307
|
+
# rule's clauses match the user.
|
308
|
+
#
|
309
|
+
# To start defining a rule, use one of the flag builder's matching methods such as
|
310
|
+
# {FlagBuilder#if_match}. This defines the first clause for the rule.
|
311
|
+
# Optionally, you may add more clauses with the rule builder's methods such as
|
312
|
+
# {#and_match} or {#and_not_match}.
|
313
|
+
# Finally, call {#then_return} to finish defining the rule.
|
314
|
+
#
|
315
|
+
class FlagRuleBuilder
|
316
|
+
# @private
|
317
|
+
FlagRuleClause = Struct.new(:attribute, :op, :values, :negate, keyword_init: true)
|
318
|
+
|
319
|
+
# @private
|
320
|
+
def initialize(flag_builder)
|
321
|
+
@flag_builder = flag_builder
|
322
|
+
@clauses = Array.new
|
323
|
+
end
|
324
|
+
|
325
|
+
# @private
|
326
|
+
def intialize_copy(other)
|
327
|
+
super(other)
|
328
|
+
@clauses = @clauses.clone
|
329
|
+
end
|
330
|
+
|
331
|
+
#
|
332
|
+
# Adds another clause, using the "is one of" operator.
|
333
|
+
#
|
334
|
+
# @example create a rule that returns `true` if the name is "Patsy" and the country is "gb"
|
335
|
+
# testData.flag("flag")
|
336
|
+
# .if_match(:name, 'Patsy')
|
337
|
+
# .and_match(:country, 'gb')
|
338
|
+
# .then_return(true)
|
339
|
+
#
|
340
|
+
# @param attribute [Symbol] the user attribute to match against
|
341
|
+
# @param values [Array<Object>] values to compare to
|
342
|
+
# @return [FlagRuleBuilder] the rule builder
|
343
|
+
#
|
344
|
+
def and_match(attribute, *values)
|
345
|
+
@clauses.push(FlagRuleClause.new(
|
346
|
+
attribute: attribute,
|
347
|
+
op: 'in',
|
348
|
+
values: values,
|
349
|
+
negate: false
|
350
|
+
))
|
351
|
+
self
|
352
|
+
end
|
353
|
+
|
354
|
+
#
|
355
|
+
# Adds another clause, using the "is not one of" operator.
|
356
|
+
#
|
357
|
+
# @example create a rule that returns `true` if the name is "Patsy" and the country is not "gb"
|
358
|
+
# testData.flag("flag")
|
359
|
+
# .if_match(:name, 'Patsy')
|
360
|
+
# .and_not_match(:country, 'gb')
|
361
|
+
# .then_return(true)
|
362
|
+
#
|
363
|
+
# @param attribute [Symbol] the user attribute to match against
|
364
|
+
# @param values [Array<Object>] values to compare to
|
365
|
+
# @return [FlagRuleBuilder] the rule builder
|
366
|
+
#
|
367
|
+
def and_not_match(attribute, *values)
|
368
|
+
@clauses.push(FlagRuleClause.new(
|
369
|
+
attribute: attribute,
|
370
|
+
op: 'in',
|
371
|
+
values: values,
|
372
|
+
negate: true
|
373
|
+
))
|
374
|
+
self
|
375
|
+
end
|
376
|
+
|
377
|
+
#
|
378
|
+
# Finishes defining the rule, specifying the result as either a boolean
|
379
|
+
# or a variation index.
|
380
|
+
#
|
381
|
+
# If the flag was previously configured with other variations and the variation specified is a boolean,
|
382
|
+
# this also changes it to a boolean flag.
|
383
|
+
#
|
384
|
+
# @param variation [Boolean, Integer] true or false or the desired variation index:
|
385
|
+
# 0 for the first, 1 for the second, etc.
|
386
|
+
# @return [FlagBuilder] the flag builder with this rule added
|
387
|
+
#
|
388
|
+
def then_return(variation)
|
389
|
+
if LaunchDarkly::Impl::Util.is_bool variation then
|
390
|
+
@variation = @flag_builder.variation_for_boolean(variation)
|
391
|
+
@flag_builder.boolean_flag.add_rule(self)
|
392
|
+
else
|
393
|
+
@variation = variation
|
394
|
+
@flag_builder.add_rule(self)
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# @private
|
399
|
+
def build(ri)
|
400
|
+
{
|
401
|
+
id: 'rule' + ri.to_s,
|
402
|
+
variation: @variation,
|
403
|
+
clauses: @clauses.collect(&:to_h)
|
404
|
+
}
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
# @private
|
409
|
+
def variation_for_boolean(variation)
|
410
|
+
variation ? TRUE_VARIATION_INDEX : FALSE_VARIATION_INDEX
|
411
|
+
end
|
412
|
+
|
413
|
+
private
|
414
|
+
|
415
|
+
TRUE_VARIATION_INDEX = 0
|
416
|
+
FALSE_VARIATION_INDEX = 1
|
417
|
+
|
418
|
+
def is_boolean_flag
|
419
|
+
@variations.size == 2 &&
|
420
|
+
@variations[TRUE_VARIATION_INDEX] == true &&
|
421
|
+
@variations[FALSE_VARIATION_INDEX] == false
|
422
|
+
end
|
423
|
+
|
424
|
+
def deep_copy_hash(from)
|
425
|
+
to = Hash.new
|
426
|
+
from.each { |k, v| to[k] = v.clone }
|
427
|
+
to
|
428
|
+
end
|
429
|
+
|
430
|
+
def deep_copy_array(from)
|
431
|
+
to = Array.new
|
432
|
+
from.each { |v| to.push(v.clone) }
|
433
|
+
to
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'ldclient-rb/impl/integrations/test_data/test_data_source'
|
2
|
+
require 'ldclient-rb/integrations/test_data/flag_builder'
|
3
|
+
|
4
|
+
require 'concurrent/atomics'
|
5
|
+
|
6
|
+
module LaunchDarkly
|
7
|
+
module Integrations
|
8
|
+
#
|
9
|
+
# A mechanism for providing dynamically updatable feature flag state in a simplified form to an SDK
|
10
|
+
# client in test scenarios.
|
11
|
+
#
|
12
|
+
# Unlike {LaunchDarkly::Integrations::FileData}, this mechanism does not use any external resources. It
|
13
|
+
# provides only the data that the application has put into it using the {#update} method.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# td = LaunchDarkly::Integrations::TestData.data_source
|
17
|
+
# td.update(td.flag("flag-key-1").variation_for_all_users(true))
|
18
|
+
# config = LaunchDarkly::Config.new(data_source: td)
|
19
|
+
# client = LaunchDarkly::LDClient.new('sdkKey', config)
|
20
|
+
# # flags can be updated at any time:
|
21
|
+
# td.update(td.flag("flag-key-2")
|
22
|
+
# .variation_for_user("some-user-key", true)
|
23
|
+
# .fallthrough_variation(false))
|
24
|
+
#
|
25
|
+
# The above example uses a simple boolean flag, but more complex configurations are possible using
|
26
|
+
# the methods of the {FlagBuilder} that is returned by {#flag}. {FlagBuilder}
|
27
|
+
# supports many of the ways a flag can be configured on the LaunchDarkly dashboard, but does not
|
28
|
+
# currently support 1. rule operators other than "in" and "not in", or 2. percentage rollouts.
|
29
|
+
#
|
30
|
+
# If the same `TestData` instance is used to configure multiple `LDClient` instances,
|
31
|
+
# any changes made to the data will propagate to all of the `LDClient`s.
|
32
|
+
#
|
33
|
+
# @since 6.3.0
|
34
|
+
#
|
35
|
+
class TestData
|
36
|
+
# Creates a new instance of the test data source.
|
37
|
+
#
|
38
|
+
# @return [TestData] a new configurable test data source
|
39
|
+
def self.data_source
|
40
|
+
self.new
|
41
|
+
end
|
42
|
+
|
43
|
+
# @private
|
44
|
+
def initialize
|
45
|
+
@flag_builders = Hash.new
|
46
|
+
@current_flags = Hash.new
|
47
|
+
@current_segments = Hash.new
|
48
|
+
@instances = Array.new
|
49
|
+
@instances_lock = Concurrent::ReadWriteLock.new
|
50
|
+
@lock = Concurrent::ReadWriteLock.new
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Called internally by the SDK to determine what arguments to pass to call
|
55
|
+
# You do not need to call this method.
|
56
|
+
#
|
57
|
+
# @private
|
58
|
+
def arity
|
59
|
+
2
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# Called internally by the SDK to associate this test data source with an {@code LDClient} instance.
|
64
|
+
# You do not need to call this method.
|
65
|
+
#
|
66
|
+
# @private
|
67
|
+
def call(_, config)
|
68
|
+
impl = LaunchDarkly::Impl::Integrations::TestData::TestDataSource.new(config.feature_store, self)
|
69
|
+
@instances_lock.with_write_lock { @instances.push(impl) }
|
70
|
+
impl
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Creates or copies a {FlagBuilder} for building a test flag configuration.
|
75
|
+
#
|
76
|
+
# If this flag key has already been defined in this `TestData` instance, then the builder
|
77
|
+
# starts with the same configuration that was last provided for this flag.
|
78
|
+
#
|
79
|
+
# Otherwise, it starts with a new default configuration in which the flag has `true` and
|
80
|
+
# `false` variations, is `true` for all users when targeting is turned on and
|
81
|
+
# `false` otherwise, and currently has targeting turned on. You can change any of those
|
82
|
+
# properties, and provide more complex behavior, using the {FlagBuilder} methods.
|
83
|
+
#
|
84
|
+
# Once you have set the desired configuration, pass the builder to {#update}.
|
85
|
+
#
|
86
|
+
# @param key [String] the flag key
|
87
|
+
# @return [FlagBuilder] a flag configuration builder
|
88
|
+
#
|
89
|
+
def flag(key)
|
90
|
+
existing_builder = @lock.with_read_lock { @flag_builders[key] }
|
91
|
+
if existing_builder.nil? then
|
92
|
+
FlagBuilder.new(key).boolean_flag
|
93
|
+
else
|
94
|
+
existing_builder.clone
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
#
|
99
|
+
# Updates the test data with the specified flag configuration.
|
100
|
+
#
|
101
|
+
# This has the same effect as if a flag were added or modified on the LaunchDarkly dashboard.
|
102
|
+
# It immediately propagates the flag change to any `LDClient` instance(s) that you have
|
103
|
+
# already configured to use this `TestData`. If no `LDClient` has been started yet,
|
104
|
+
# it simply adds this flag to the test data which will be provided to any `LDClient` that
|
105
|
+
# you subsequently configure.
|
106
|
+
#
|
107
|
+
# Any subsequent changes to this {FlagBuilder} instance do not affect the test data,
|
108
|
+
# unless you call {#update} again.
|
109
|
+
#
|
110
|
+
# @param flag_builder [FlagBuilder] a flag configuration builder
|
111
|
+
# @return [TestData] the TestData instance
|
112
|
+
#
|
113
|
+
def update(flag_builder)
|
114
|
+
new_flag = nil
|
115
|
+
@lock.with_write_lock do
|
116
|
+
@flag_builders[flag_builder.key] = flag_builder
|
117
|
+
version = 0
|
118
|
+
flag_key = flag_builder.key.to_sym
|
119
|
+
if @current_flags[flag_key] then
|
120
|
+
version = @current_flags[flag_key][:version]
|
121
|
+
end
|
122
|
+
new_flag = flag_builder.build(version+1)
|
123
|
+
@current_flags[flag_key] = new_flag
|
124
|
+
end
|
125
|
+
update_item(FEATURES, new_flag)
|
126
|
+
self
|
127
|
+
end
|
128
|
+
|
129
|
+
#
|
130
|
+
# Copies a full feature flag data model object into the test data.
|
131
|
+
#
|
132
|
+
# It immediately propagates the flag change to any `LDClient` instance(s) that you have already
|
133
|
+
# configured to use this `TestData`. If no `LDClient` has been started yet, it simply adds
|
134
|
+
# this flag to the test data which will be provided to any LDClient that you subsequently
|
135
|
+
# configure.
|
136
|
+
#
|
137
|
+
# Use this method if you need to use advanced flag configuration properties that are not supported by
|
138
|
+
# the simplified {FlagBuilder} API. Otherwise it is recommended to use the regular {flag}/{update}
|
139
|
+
# mechanism to avoid dependencies on details of the data model.
|
140
|
+
#
|
141
|
+
# You cannot make incremental changes with {flag}/{update} to a flag that has been added in this way;
|
142
|
+
# you can only replace it with an entirely new flag configuration.
|
143
|
+
#
|
144
|
+
# @param flag [Hash] the flag configuration
|
145
|
+
# @return [TestData] the TestData instance
|
146
|
+
#
|
147
|
+
def use_preconfigured_flag(flag)
|
148
|
+
use_preconfigured_item(FEATURES, flag, @current_flags)
|
149
|
+
end
|
150
|
+
|
151
|
+
#
|
152
|
+
# Copies a full user segment data model object into the test data.
|
153
|
+
#
|
154
|
+
# It immediately propagates the change to any `LDClient` instance(s) that you have already
|
155
|
+
# configured to use this `TestData`. If no `LDClient` has been started yet, it simply adds
|
156
|
+
# this segment to the test data which will be provided to any LDClient that you subsequently
|
157
|
+
# configure.
|
158
|
+
#
|
159
|
+
# This method is currently the only way to inject user segment data, since there is no builder
|
160
|
+
# API for segments. It is mainly intended for the SDK's own tests of user segment functionality,
|
161
|
+
# since application tests that need to produce a desired evaluation state could do so more easily
|
162
|
+
# by just setting flag values.
|
163
|
+
#
|
164
|
+
# @param segment [Hash] the segment configuration
|
165
|
+
# @return [TestData] the TestData instance
|
166
|
+
#
|
167
|
+
def use_preconfigured_segment(segment)
|
168
|
+
use_preconfigured_item(SEGMENTS, segment, @current_segments)
|
169
|
+
end
|
170
|
+
|
171
|
+
private def use_preconfigured_item(kind, item, current)
|
172
|
+
key = item[:key].to_sym
|
173
|
+
@lock.with_write_lock do
|
174
|
+
old_item = current[key]
|
175
|
+
if !old_item.nil? then
|
176
|
+
item = item.clone
|
177
|
+
item[:version] = old_item[:version] + 1
|
178
|
+
end
|
179
|
+
current[key] = item
|
180
|
+
end
|
181
|
+
update_item(kind, item)
|
182
|
+
self
|
183
|
+
end
|
184
|
+
|
185
|
+
private def update_item(kind, item)
|
186
|
+
@instances_lock.with_read_lock do
|
187
|
+
@instances.each do | instance |
|
188
|
+
instance.upsert(kind, item)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# @private
|
194
|
+
def make_init_data
|
195
|
+
@lock.with_read_lock do
|
196
|
+
{
|
197
|
+
FEATURES => @current_flags.clone,
|
198
|
+
SEGMENTS => @current_segments.clone
|
199
|
+
}
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# @private
|
204
|
+
def closed_instance(instance)
|
205
|
+
@instances_lock.with_write_lock { @instances.delete(instance) }
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -4,6 +4,11 @@ require "ldclient-rb/expiring_cache"
|
|
4
4
|
|
5
5
|
module LaunchDarkly
|
6
6
|
module Integrations
|
7
|
+
#
|
8
|
+
# Support code that may be helpful in creating integrations.
|
9
|
+
#
|
10
|
+
# @since 5.5.0
|
11
|
+
#
|
7
12
|
module Util
|
8
13
|
#
|
9
14
|
# CachingStoreWrapper is a partial implementation of the {LaunchDarkly::Interfaces::FeatureStore}
|