launchdarkly-server-sdk 6.2.5 → 7.0.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/lib/ldclient-rb/config.rb +203 -43
  4. data/lib/ldclient-rb/context.rb +487 -0
  5. data/lib/ldclient-rb/evaluation_detail.rb +85 -26
  6. data/lib/ldclient-rb/events.rb +185 -146
  7. data/lib/ldclient-rb/flags_state.rb +25 -14
  8. data/lib/ldclient-rb/impl/big_segments.rb +117 -0
  9. data/lib/ldclient-rb/impl/context.rb +96 -0
  10. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  11. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  12. data/lib/ldclient-rb/impl/evaluator.rb +428 -132
  13. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  14. data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
  15. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  16. data/lib/ldclient-rb/impl/event_sender.rb +6 -6
  17. data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
  18. data/lib/ldclient-rb/impl/event_types.rb +78 -0
  19. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +7 -7
  20. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +92 -28
  21. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +212 -0
  22. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +165 -32
  23. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
  24. data/lib/ldclient-rb/impl/model/clause.rb +39 -0
  25. data/lib/ldclient-rb/impl/model/feature_flag.rb +213 -0
  26. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
  27. data/lib/ldclient-rb/impl/model/segment.rb +126 -0
  28. data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
  29. data/lib/ldclient-rb/impl/repeating_task.rb +47 -0
  30. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  31. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  32. data/lib/ldclient-rb/impl/util.rb +62 -1
  33. data/lib/ldclient-rb/in_memory_store.rb +2 -2
  34. data/lib/ldclient-rb/integrations/consul.rb +9 -2
  35. data/lib/ldclient-rb/integrations/dynamodb.rb +47 -2
  36. data/lib/ldclient-rb/integrations/file_data.rb +108 -0
  37. data/lib/ldclient-rb/integrations/redis.rb +43 -3
  38. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +594 -0
  39. data/lib/ldclient-rb/integrations/test_data.rb +213 -0
  40. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +14 -9
  41. data/lib/ldclient-rb/integrations.rb +2 -51
  42. data/lib/ldclient-rb/interfaces.rb +151 -1
  43. data/lib/ldclient-rb/ldclient.rb +175 -133
  44. data/lib/ldclient-rb/memoized_value.rb +1 -1
  45. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  46. data/lib/ldclient-rb/polling.rb +22 -41
  47. data/lib/ldclient-rb/reference.rb +274 -0
  48. data/lib/ldclient-rb/requestor.rb +7 -7
  49. data/lib/ldclient-rb/stream.rb +9 -9
  50. data/lib/ldclient-rb/util.rb +11 -17
  51. data/lib/ldclient-rb/version.rb +1 -1
  52. data/lib/ldclient-rb.rb +2 -4
  53. metadata +49 -23
  54. data/lib/ldclient-rb/event_summarizer.rb +0 -55
  55. data/lib/ldclient-rb/file_data_source.rb +0 -314
  56. data/lib/ldclient-rb/impl/event_factory.rb +0 -126
  57. data/lib/ldclient-rb/newrelic.rb +0 -17
  58. data/lib/ldclient-rb/redis_store.rb +0 -88
  59. data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -0,0 +1,487 @@
1
+ require 'set'
2
+ require 'ldclient-rb/impl/context'
3
+ require 'ldclient-rb/reference'
4
+
5
+ module LaunchDarkly
6
+ # LDContext is a collection of attributes that can be referenced in flag
7
+ # evaluations and analytics events.
8
+ #
9
+ # To create an LDContext of a single kind, such as a user, you may use
10
+ # {LDContext#create} or {LDContext#with_key}.
11
+ #
12
+ # To create an LDContext with multiple kinds, use {LDContext#create_multi}.
13
+ #
14
+ # Each factory method will always return an LDContext. However, that
15
+ # LDContext may be invalid. You can check the validity of the resulting
16
+ # context, and the associated errors by calling {LDContext#valid?} and
17
+ # {LDContext#error}
18
+ class LDContext
19
+ KIND_DEFAULT = "user"
20
+ KIND_MULTI = "multi"
21
+
22
+ ERR_NOT_HASH = 'context data is not a hash'
23
+ private_constant :ERR_NOT_HASH
24
+ ERR_KEY_EMPTY = 'context key must not be null or empty'
25
+ private_constant :ERR_KEY_EMPTY
26
+ ERR_KIND_MULTI_NON_CONTEXT_ARRAY = 'context data must be an array of valid LDContexts'
27
+ private_constant :ERR_KIND_MULTI_NON_CONTEXT_ARRAY
28
+ ERR_KIND_MULTI_CANNOT_CONTAIN_MULTI = 'multi-kind context cannot contain another multi-kind context'
29
+ private_constant :ERR_KIND_MULTI_CANNOT_CONTAIN_MULTI
30
+ ERR_KIND_MULTI_WITH_NO_KINDS = 'multi-context must contain at least one kind'
31
+ private_constant :ERR_KIND_MULTI_WITH_NO_KINDS
32
+ ERR_KIND_MULTI_DUPLICATES = 'multi-kind context cannot have same kind more than once'
33
+ private_constant :ERR_KIND_MULTI_DUPLICATES
34
+ ERR_CUSTOM_NON_HASH = 'context custom must be a hash'
35
+ private_constant :ERR_CUSTOM_NON_HASH
36
+ ERR_PRIVATE_NON_ARRAY = 'context private attributes must be an array'
37
+
38
+ # @return [String, nil] Returns the key for this context
39
+ attr_reader :key
40
+
41
+ # @return [String, nil] Returns the fully qualified key for this context
42
+ attr_reader :fully_qualified_key
43
+
44
+ # @return [String, nil] Returns the kind for this context
45
+ attr_reader :kind
46
+
47
+ # @return [String, nil] Returns the error associated with this LDContext if invalid
48
+ attr_reader :error
49
+
50
+ # @return [Array<Reference>] Returns the private attributes associated with this LDContext
51
+ attr_reader :private_attributes
52
+
53
+ #
54
+ # @private
55
+ # @param key [String, nil]
56
+ # @param fully_qualified_key [String, nil]
57
+ # @param kind [String, nil]
58
+ # @param name [String, nil]
59
+ # @param anonymous [Boolean, nil]
60
+ # @param attributes [Hash, nil]
61
+ # @param private_attributes [Array<String>, nil]
62
+ # @param error [String, nil]
63
+ # @param contexts [Array<LDContext>, nil]
64
+ #
65
+ def initialize(key, fully_qualified_key, kind, name = nil, anonymous = nil, attributes = nil, private_attributes = nil, error = nil, contexts = nil)
66
+ @key = key
67
+ @fully_qualified_key = fully_qualified_key
68
+ @kind = kind
69
+ @name = name
70
+ @anonymous = anonymous || false
71
+ @attributes = attributes
72
+ @private_attributes = []
73
+ (private_attributes || []).each do |attribute|
74
+ reference = Reference.create(attribute)
75
+ @private_attributes << reference if reference.error.nil?
76
+ end
77
+ @error = error
78
+ @contexts = contexts
79
+ @is_multi = !contexts.nil?
80
+ end
81
+ private_class_method :new
82
+
83
+ #
84
+ # @return [Boolean] Is this LDContext a multi-kind context?
85
+ #
86
+ def multi_kind?
87
+ @is_multi
88
+ end
89
+
90
+ #
91
+ # @return [Boolean] Determine if this LDContext is considered valid
92
+ #
93
+ def valid?
94
+ @error.nil?
95
+ end
96
+
97
+ #
98
+ # Returns a hash mapping each context's kind to its key.
99
+ #
100
+ # @return [Hash<Symbol, String>]
101
+ #
102
+ def keys
103
+ return {} unless valid?
104
+ return Hash[kind, key] unless multi_kind?
105
+
106
+ @contexts.map { |c| [c.kind, c.key] }.to_h
107
+ end
108
+
109
+ #
110
+ # Returns an array of context kinds.
111
+ #
112
+ # @return [Array<String>]
113
+ #
114
+ def kinds
115
+ return [] unless valid?
116
+ return [kind] unless multi_kind?
117
+
118
+ @contexts.map { |c| c.kind }
119
+ end
120
+
121
+ #
122
+ # Return an array of top level attribute keys (excluding built-in attributes)
123
+ #
124
+ # @return [Array<Symbol>]
125
+ #
126
+ def get_custom_attribute_names
127
+ return [] if @attributes.nil?
128
+
129
+ @attributes.keys
130
+ end
131
+
132
+ #
133
+ # get_value looks up the value of any attribute of the Context by name.
134
+ # This includes only attributes that are addressable in evaluations-- not
135
+ # metadata such as private attributes.
136
+ #
137
+ # For a single-kind context, the attribute name can be any custom attribute.
138
+ # It can also be one of the built-in ones like "kind", "key", or "name".
139
+ #
140
+ # For a multi-kind context, the only supported attribute name is "kind".
141
+ # Use {#individual_context} to inspect a Context for a particular kind and
142
+ # then get its attributes.
143
+ #
144
+ # This method does not support complex expressions for getting individual
145
+ # values out of JSON objects or arrays, such as "/address/street". Use
146
+ # {#get_value_for_reference} for that purpose.
147
+ #
148
+ # If the value is found, the return value is the attribute value;
149
+ # otherwise, it is nil.
150
+ #
151
+ # @param attribute [String, Symbol]
152
+ # @return [any]
153
+ #
154
+ def get_value(attribute)
155
+ reference = Reference.create_literal(attribute)
156
+ get_value_for_reference(reference)
157
+ end
158
+
159
+ #
160
+ # get_value_for_reference looks up the value of any attribute of the
161
+ # Context, or a value contained within an attribute, based on a {Reference}
162
+ # instance. This includes only attributes that are addressable in
163
+ # evaluations-- not metadata such as private attributes.
164
+ #
165
+ # This implements the same behavior that the SDK uses to resolve attribute
166
+ # references during a flag evaluation. In a single-kind context, the
167
+ # {Reference} can represent a simple attribute name-- either a built-in one
168
+ # like "name" or "key", or a custom attribute -- or, it can be a
169
+ # slash-delimited path using a JSON-Pointer-like syntax. See {Reference}
170
+ # for more details.
171
+ #
172
+ # For a multi-kind context, the only supported attribute name is "kind".
173
+ # Use {#individual_context} to inspect a Context for a particular kind and
174
+ # then get its attributes.
175
+ #
176
+ # If the value is found, the return value is the attribute value;
177
+ # otherwise, it is nil.
178
+ #
179
+ # @param reference [Reference]
180
+ # @return [any]
181
+ #
182
+ def get_value_for_reference(reference)
183
+ return nil unless valid?
184
+ return nil unless reference.is_a?(Reference)
185
+ return nil unless reference.error.nil?
186
+
187
+ first_component = reference.component(0)
188
+ return nil if first_component.nil?
189
+
190
+ if multi_kind?
191
+ if reference.depth == 1 && first_component == :kind
192
+ return kind
193
+ end
194
+
195
+ # Multi-kind contexts have no other addressable attributes
196
+ return nil
197
+ end
198
+
199
+ value = get_top_level_addressable_attribute_single_kind(first_component)
200
+ return nil if value.nil?
201
+
202
+ (1...reference.depth).each do |i|
203
+ name = reference.component(i)
204
+
205
+ return nil unless value.is_a?(Hash)
206
+ return nil unless value.has_key?(name)
207
+
208
+ value = value[name]
209
+ end
210
+
211
+ value
212
+ end
213
+
214
+ #
215
+ # Returns the number of context kinds in this context.
216
+ #
217
+ # For a valid individual context, this returns 1. For a multi-context, it
218
+ # returns the number of context kinds. For an invalid context, it returns
219
+ # zero.
220
+ #
221
+ # @return [Integer] the number of context kinds
222
+ #
223
+ def individual_context_count
224
+ return 0 unless valid?
225
+ return 1 if @contexts.nil?
226
+ @contexts.count
227
+ end
228
+
229
+ #
230
+ # Returns the single-kind LDContext corresponding to one of the kinds in
231
+ # this context.
232
+ #
233
+ # The `kind` parameter can be either a number representing a zero-based
234
+ # index, or a string representing a context kind.
235
+ #
236
+ # If this method is called on a single-kind LDContext, then the only
237
+ # allowable value for `kind` is either zero or the same value as {#kind},
238
+ # and the return value on success is the same LDContext.
239
+ #
240
+ # If the method is called on a multi-context, and `kind` is a number, it
241
+ # must be a non-negative index that is less than the number of kinds (that
242
+ # is, less than the return value of {#individual_context_count}, and the
243
+ # return value on success is one of the individual LDContexts within. Or,
244
+ # if `kind` is a string, it must match the context kind of one of the
245
+ # individual contexts.
246
+ #
247
+ # If there is no context corresponding to `kind`, the method returns nil.
248
+ #
249
+ # @param kind [Integer, String] the index or string value of a context kind
250
+ # @return [LDContext, nil] the context corresponding to that index or kind,
251
+ # or null if none.
252
+ #
253
+ def individual_context(kind)
254
+ return nil unless valid?
255
+
256
+ if kind.is_a?(Integer)
257
+ unless multi_kind?
258
+ return kind == 0 ? self : nil
259
+ end
260
+
261
+ return kind >= 0 && kind < @contexts.count ? @contexts[kind] : nil
262
+ end
263
+
264
+ return nil unless kind.is_a?(String)
265
+
266
+ unless multi_kind?
267
+ return self.kind == kind ? self : nil
268
+ end
269
+
270
+ @contexts.each do |context|
271
+ return context if context.kind == kind
272
+ end
273
+
274
+ nil
275
+ end
276
+
277
+ #
278
+ # Retrieve the value of any top level, addressable attribute.
279
+ #
280
+ # This method returns an array of two values. The first element is the
281
+ # value of the requested attribute or nil if it does not exist. The second
282
+ # value will be true if the attribute exists; otherwise, it will be false.
283
+ #
284
+ # @param name [Symbol]
285
+ # @return [any]
286
+ #
287
+ private def get_top_level_addressable_attribute_single_kind(name)
288
+ case name
289
+ when :kind
290
+ kind
291
+ when :key
292
+ key
293
+ when :name
294
+ @name
295
+ when :anonymous
296
+ @anonymous
297
+ else
298
+ @attributes&.fetch(name, nil)
299
+ end
300
+ end
301
+
302
+ #
303
+ # Convenience method to create a simple single kind context providing only
304
+ # a key and kind type.
305
+ #
306
+ # @param key [String]
307
+ # @param kind [String]
308
+ #
309
+ def self.with_key(key, kind = KIND_DEFAULT)
310
+ create({key: key, kind: kind})
311
+ end
312
+
313
+ #
314
+ # Create a single kind context from the provided hash.
315
+ #
316
+ # The provided hash must match the format as outlined in the
317
+ # {https://docs.launchdarkly.com/sdk/features/user-config SDK
318
+ # documentation}.
319
+ #
320
+ # @param data [Hash]
321
+ # @return [LDContext]
322
+ #
323
+ def self.create(data)
324
+ return create_invalid_context(ERR_NOT_HASH) unless data.is_a?(Hash)
325
+ return create_legacy_context(data) unless data.has_key?(:kind)
326
+
327
+ kind = data[:kind]
328
+ if kind == KIND_MULTI
329
+ contexts = []
330
+ data.each do |key, value|
331
+ next if key == :kind
332
+ contexts << create_single_context(value, key.to_s)
333
+ end
334
+
335
+ return create_multi(contexts)
336
+ end
337
+
338
+ create_single_context(data, kind)
339
+ end
340
+
341
+ #
342
+ # Create a multi-kind context from the array of LDContexts provided.
343
+ #
344
+ # A multi-kind context is comprised of two or more single kind contexts.
345
+ # You cannot include a multi-kind context instead another multi-kind
346
+ # context.
347
+ #
348
+ # Additionally, the kind of each single-kind context must be unique. For
349
+ # instance, you cannot create a multi-kind context that includes two user
350
+ # kind contexts.
351
+ #
352
+ # If you attempt to create a multi-kind context from one single-kind
353
+ # context, this method will return the single-kind context instead of a new
354
+ # multi-kind context wrapping that one single-kind.
355
+ #
356
+ # @param contexts [Array<LDContext>]
357
+ # @return [LDContext]
358
+ #
359
+ def self.create_multi(contexts)
360
+ return create_invalid_context(ERR_KIND_MULTI_NON_CONTEXT_ARRAY) unless contexts.is_a?(Array)
361
+ return create_invalid_context(ERR_KIND_MULTI_WITH_NO_KINDS) if contexts.empty?
362
+
363
+ kinds = Set.new
364
+ contexts.each do |context|
365
+ if !context.is_a?(LDContext)
366
+ return create_invalid_context(ERR_KIND_MULTI_NON_CONTEXT_ARRAY)
367
+ elsif !context.valid?
368
+ return create_invalid_context(ERR_KIND_MULTI_NON_CONTEXT_ARRAY)
369
+ elsif context.multi_kind?
370
+ return create_invalid_context(ERR_KIND_MULTI_CANNOT_CONTAIN_MULTI)
371
+ elsif kinds.include? context.kind
372
+ return create_invalid_context(ERR_KIND_MULTI_DUPLICATES)
373
+ end
374
+
375
+ kinds.add(context.kind)
376
+ end
377
+
378
+ return contexts[0] if contexts.length == 1
379
+
380
+ full_key = contexts.sort_by(&:kind)
381
+ .map { |c| LaunchDarkly::Impl::Context::canonicalize_key_for_kind(c.kind, c.key) }
382
+ .join(":")
383
+
384
+ new(nil, full_key, "multi", nil, false, nil, nil, nil, contexts)
385
+ end
386
+
387
+ #
388
+ # @param error [String]
389
+ # @return [LDContext]
390
+ #
391
+ private_class_method def self.create_invalid_context(error)
392
+ new(nil, nil, nil, nil, false, nil, nil, error)
393
+ end
394
+
395
+ #
396
+ # @param data [Hash]
397
+ # @return [LDContext]
398
+ #
399
+ private_class_method def self.create_legacy_context(data)
400
+ key = data[:key]
401
+
402
+ # Legacy users are allowed to have "" as a key but they cannot have nil as a key.
403
+ return create_invalid_context(ERR_KEY_EMPTY) if key.nil?
404
+
405
+ name = data[:name]
406
+ name_error = LaunchDarkly::Impl::Context.validate_name(name)
407
+ return create_invalid_context(name_error) unless name_error.nil?
408
+
409
+ anonymous = data[:anonymous]
410
+ anonymous_error = LaunchDarkly::Impl::Context.validate_anonymous(anonymous, true)
411
+ return create_invalid_context(anonymous_error) unless anonymous_error.nil?
412
+
413
+ custom = data[:custom]
414
+ unless custom.nil? || custom.is_a?(Hash)
415
+ return create_invalid_context(ERR_CUSTOM_NON_HASH)
416
+ end
417
+
418
+ # We only need to create an attribute hash if one of these keys exist.
419
+ # Everything else is stored in dedicated instance variables.
420
+ attributes = custom.clone
421
+ data.each do |k, v|
422
+ case k
423
+ when :ip, :email, :avatar, :firstName, :lastName, :country
424
+ attributes ||= {}
425
+ attributes[k] = v.clone
426
+ else
427
+ next
428
+ end
429
+ end
430
+
431
+ private_attributes = data[:privateAttributeNames]
432
+ if private_attributes && !private_attributes.is_a?(Array)
433
+ return create_invalid_context(ERR_PRIVATE_NON_ARRAY)
434
+ end
435
+
436
+ new(key.to_s, key.to_s, KIND_DEFAULT, name, anonymous, attributes, private_attributes)
437
+ end
438
+
439
+ #
440
+ # @param data [Hash]
441
+ # @param kind [String]
442
+ # @return [LaunchDarkly::LDContext]
443
+ #
444
+ private_class_method def self.create_single_context(data, kind)
445
+ unless data.is_a?(Hash)
446
+ return create_invalid_context(ERR_NOT_HASH)
447
+ end
448
+
449
+ kind_error = LaunchDarkly::Impl::Context.validate_kind(kind)
450
+ return create_invalid_context(kind_error) unless kind_error.nil?
451
+
452
+ key = data[:key]
453
+ key_error = LaunchDarkly::Impl::Context.validate_key(key)
454
+ return create_invalid_context(key_error) unless key_error.nil?
455
+
456
+ name = data[:name]
457
+ name_error = LaunchDarkly::Impl::Context.validate_name(name)
458
+ return create_invalid_context(name_error) unless name_error.nil?
459
+
460
+ anonymous = data.fetch(:anonymous, false)
461
+ anonymous_error = LaunchDarkly::Impl::Context.validate_anonymous(anonymous, false)
462
+ return create_invalid_context(anonymous_error) unless anonymous_error.nil?
463
+
464
+ meta = data.fetch(:_meta, {})
465
+ private_attributes = meta[:privateAttributes]
466
+ if private_attributes && !private_attributes.is_a?(Array)
467
+ return create_invalid_context(ERR_PRIVATE_NON_ARRAY)
468
+ end
469
+
470
+ # We only need to create an attribute hash if there are keys set outside
471
+ # of the ones we store in dedicated instance variables.
472
+ attributes = nil
473
+ data.each do |k, v|
474
+ case k
475
+ when :kind, :key, :name, :anonymous, :_meta
476
+ next
477
+ else
478
+ attributes ||= {}
479
+ attributes[k] = v.clone
480
+ end
481
+ end
482
+
483
+ full_key = kind == LDContext::KIND_DEFAULT ? key.to_s : LaunchDarkly::Impl::Context::canonicalize_key_for_kind(kind, key.to_s)
484
+ new(key.to_s, full_key, kind, name, anonymous, attributes, private_attributes)
485
+ end
486
+ end
487
+ end