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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -5
  3. data/lib/ldclient-rb/config.rb +118 -4
  4. data/lib/ldclient-rb/evaluation_detail.rb +104 -14
  5. data/lib/ldclient-rb/events.rb +201 -107
  6. data/lib/ldclient-rb/file_data_source.rb +9 -300
  7. data/lib/ldclient-rb/flags_state.rb +23 -12
  8. data/lib/ldclient-rb/impl/big_segments.rb +117 -0
  9. data/lib/ldclient-rb/impl/diagnostic_events.rb +1 -1
  10. data/lib/ldclient-rb/impl/evaluator.rb +116 -62
  11. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +22 -9
  12. data/lib/ldclient-rb/impl/evaluator_helpers.rb +53 -0
  13. data/lib/ldclient-rb/impl/evaluator_operators.rb +1 -1
  14. data/lib/ldclient-rb/impl/event_summarizer.rb +63 -0
  15. data/lib/ldclient-rb/impl/event_types.rb +90 -0
  16. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +82 -18
  17. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +212 -0
  18. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +84 -31
  19. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
  20. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +177 -0
  21. data/lib/ldclient-rb/impl/model/serialization.rb +7 -37
  22. data/lib/ldclient-rb/impl/repeating_task.rb +47 -0
  23. data/lib/ldclient-rb/impl/util.rb +62 -1
  24. data/lib/ldclient-rb/integrations/consul.rb +8 -1
  25. data/lib/ldclient-rb/integrations/dynamodb.rb +48 -3
  26. data/lib/ldclient-rb/integrations/file_data.rb +108 -0
  27. data/lib/ldclient-rb/integrations/redis.rb +42 -2
  28. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +438 -0
  29. data/lib/ldclient-rb/integrations/test_data.rb +209 -0
  30. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +5 -0
  31. data/lib/ldclient-rb/integrations.rb +2 -51
  32. data/lib/ldclient-rb/interfaces.rb +152 -2
  33. data/lib/ldclient-rb/ldclient.rb +131 -33
  34. data/lib/ldclient-rb/polling.rb +22 -41
  35. data/lib/ldclient-rb/requestor.rb +3 -3
  36. data/lib/ldclient-rb/stream.rb +4 -3
  37. data/lib/ldclient-rb/util.rb +10 -1
  38. data/lib/ldclient-rb/version.rb +1 -1
  39. data/lib/ldclient-rb.rb +0 -1
  40. metadata +35 -132
  41. data/.circleci/config.yml +0 -40
  42. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -37
  43. data/.github/ISSUE_TEMPLATE/config.yml +0 -5
  44. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  45. data/.github/pull_request_template.md +0 -21
  46. data/.gitignore +0 -16
  47. data/.hound.yml +0 -2
  48. data/.ldrelease/build-docs.sh +0 -18
  49. data/.ldrelease/circleci/linux/execute.sh +0 -18
  50. data/.ldrelease/circleci/mac/execute.sh +0 -18
  51. data/.ldrelease/circleci/template/build.sh +0 -29
  52. data/.ldrelease/circleci/template/publish.sh +0 -23
  53. data/.ldrelease/circleci/template/set-gem-home.sh +0 -7
  54. data/.ldrelease/circleci/template/test.sh +0 -10
  55. data/.ldrelease/circleci/template/update-version.sh +0 -8
  56. data/.ldrelease/circleci/windows/execute.ps1 +0 -19
  57. data/.ldrelease/config.yml +0 -29
  58. data/.rspec +0 -2
  59. data/.rubocop.yml +0 -600
  60. data/.simplecov +0 -4
  61. data/CHANGELOG.md +0 -351
  62. data/CODEOWNERS +0 -1
  63. data/CONTRIBUTING.md +0 -37
  64. data/Gemfile +0 -3
  65. data/azure-pipelines.yml +0 -51
  66. data/docs/Makefile +0 -26
  67. data/docs/index.md +0 -9
  68. data/launchdarkly-server-sdk.gemspec +0 -45
  69. data/lib/ldclient-rb/event_summarizer.rb +0 -55
  70. data/lib/ldclient-rb/impl/event_factory.rb +0 -120
  71. data/spec/config_spec.rb +0 -63
  72. data/spec/diagnostic_events_spec.rb +0 -163
  73. data/spec/evaluation_detail_spec.rb +0 -135
  74. data/spec/event_sender_spec.rb +0 -197
  75. data/spec/event_summarizer_spec.rb +0 -63
  76. data/spec/events_spec.rb +0 -607
  77. data/spec/expiring_cache_spec.rb +0 -76
  78. data/spec/feature_store_spec_base.rb +0 -213
  79. data/spec/file_data_source_spec.rb +0 -283
  80. data/spec/fixtures/feature.json +0 -37
  81. data/spec/fixtures/feature1.json +0 -36
  82. data/spec/fixtures/user.json +0 -9
  83. data/spec/flags_state_spec.rb +0 -81
  84. data/spec/http_util.rb +0 -132
  85. data/spec/impl/evaluator_bucketing_spec.rb +0 -111
  86. data/spec/impl/evaluator_clause_spec.rb +0 -55
  87. data/spec/impl/evaluator_operators_spec.rb +0 -141
  88. data/spec/impl/evaluator_rule_spec.rb +0 -96
  89. data/spec/impl/evaluator_segment_spec.rb +0 -125
  90. data/spec/impl/evaluator_spec.rb +0 -305
  91. data/spec/impl/evaluator_spec_base.rb +0 -75
  92. data/spec/impl/model/serialization_spec.rb +0 -41
  93. data/spec/in_memory_feature_store_spec.rb +0 -12
  94. data/spec/integrations/consul_feature_store_spec.rb +0 -40
  95. data/spec/integrations/dynamodb_feature_store_spec.rb +0 -103
  96. data/spec/integrations/store_wrapper_spec.rb +0 -276
  97. data/spec/launchdarkly-server-sdk_spec.rb +0 -13
  98. data/spec/launchdarkly-server-sdk_spec_autoloadtest.rb +0 -9
  99. data/spec/ldclient_end_to_end_spec.rb +0 -157
  100. data/spec/ldclient_spec.rb +0 -643
  101. data/spec/newrelic_spec.rb +0 -5
  102. data/spec/polling_spec.rb +0 -120
  103. data/spec/redis_feature_store_spec.rb +0 -121
  104. data/spec/requestor_spec.rb +0 -196
  105. data/spec/segment_store_spec_base.rb +0 -95
  106. data/spec/simple_lru_cache_spec.rb +0 -24
  107. data/spec/spec_helper.rb +0 -9
  108. data/spec/store_spec.rb +0 -10
  109. data/spec/stream_spec.rb +0 -45
  110. data/spec/user_filter_spec.rb +0 -91
  111. data/spec/util_spec.rb +0 -17
  112. 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}