launchdarkly-server-sdk 6.2.5 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
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