Class: LaunchDarkly::LDContext

Inherits:
Object
  • Object
show all
Defined in:
lib/ldclient-rb/context.rb

Overview

LDContext is a collection of attributes that can be referenced in flag evaluations and analytics events.

Every attribute name must be a symbol. This applies to the built-in properties, such as :key and :kind, and to custom attributes at every level of nesting. The SDK addresses attributes by symbol, so it cannot find an attribute that has a string name. Such an attribute is invisible to flag evaluation and to private attribute redaction.

Take care with data that arrives as JSON. JSON.parse returns string keys by default, so pass symbolize_names: true before you build a context from it. Note that the "name": value form in a hash literal already produces a symbol, so a hand-written literal is safe.

To create an LDContext of a single kind, such as a user, you may use LDContext.create or LDContext.with_key.

To create an LDContext with multiple kinds, use LDContext.create_multi.

Each factory method will always return an LDContext. However, that LDContext may be invalid. You can check the validity of the resulting context, and the associated errors by calling #valid? and #error

Constant Summary collapse

KIND_DEFAULT =
"user"
KIND_MULTI =
"multi"
ERR_PRIVATE_NON_ARRAY =
'context private attributes must be an array'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key, fully_qualified_key, kind, name = nil, anonymous = nil, attributes = nil, private_attributes = nil, error = nil, contexts = nil) ⇒ LDContext

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a new instance of LDContext.

Parameters:

  • key (String, nil)
  • fully_qualified_key (String, nil)
  • kind (String, nil)
  • name (String, nil) (defaults to: nil)
  • anonymous (Boolean, nil) (defaults to: nil)
  • attributes (Hash, nil) (defaults to: nil)
  • private_attributes (Array<String>, nil) (defaults to: nil)
  • error (String, nil) (defaults to: nil)
  • contexts (Array<LDContext>, nil) (defaults to: nil)


73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/ldclient-rb/context.rb', line 73

def initialize(key, fully_qualified_key, kind, name = nil, anonymous = nil, attributes = nil, private_attributes = nil, error = nil, contexts = nil)
  @key = key
  @fully_qualified_key = fully_qualified_key
  @kind = kind
  @name = name
  @anonymous = anonymous || false
  @attributes = attributes
  @private_attributes = Set.new
  (private_attributes || []).each do |attribute|
    reference = Reference.create(attribute)
    @private_attributes.add(reference) if reference.error.nil?
  end
  @error = error
  @contexts = contexts
  @is_multi = !contexts.nil?
end

Instance Attribute Details

#errorString? (readonly)

Returns the error associated with this LDContext if invalid

Returns:

  • (String, nil)

    Returns the error associated with this LDContext if invalid



59
60
61
# File 'lib/ldclient-rb/context.rb', line 59

def error
  @error
end

#fully_qualified_keyString? (readonly)

Returns the fully qualified key for this context

Returns:

  • (String, nil)

    Returns the fully qualified key for this context



53
54
55
# File 'lib/ldclient-rb/context.rb', line 53

def fully_qualified_key
  @fully_qualified_key
end

#keyString? (readonly)

Returns the key for this context

Returns:

  • (String, nil)

    Returns the key for this context



50
51
52
# File 'lib/ldclient-rb/context.rb', line 50

def key
  @key
end

#kindString? (readonly)

Returns the kind for this context

Returns:

  • (String, nil)

    Returns the kind for this context



56
57
58
# File 'lib/ldclient-rb/context.rb', line 56

def kind
  @kind
end

Class Method Details

.create(data) ⇒ LDContext

Create a single kind context from the provided hash.

The provided hash must match the format as outlined in the SDK documentation.

Every key in the hash must be a symbol, at the top level and inside any nested attribute value. Refer to the LaunchDarkly::LDContext class documentation for why, and for the JSON.parse caveat.

A string key does not make the context invalid, with two exceptions. The context requires a symbol :kind and a symbol :key, so a hash that supplies those as strings is invalid.

Parameters:

  • data (Hash)

Returns:



475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
# File 'lib/ldclient-rb/context.rb', line 475

def self.create(data)
  return create_invalid_context(ERR_NOT_HASH) unless data.is_a?(Hash)

  kind = data[:kind]
  if kind == KIND_MULTI
    contexts = []
    data.each do |key, value|
      next if key == :kind
      contexts << create_single_context(value, key.to_s)
    end

    return create_multi(contexts)
  end

  create_single_context(data, kind)
end

.create_multi(contexts) ⇒ LDContext

Create a multi-kind context from the array of LDContexts provided.

A multi-kind context is comprised of two or more single kind contexts. You cannot include a multi-kind context instead another multi-kind context.

Additionally, the kind of each single-kind context must be unique. For instance, you cannot create a multi-kind context that includes two user kind contexts.

If you attempt to create a multi-kind context from one single-kind context, this method will return the single-kind context instead of a new multi-kind context wrapping that one single-kind.

Parameters:

Returns:



510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
# File 'lib/ldclient-rb/context.rb', line 510

def self.create_multi(contexts)
  return create_invalid_context(ERR_KIND_MULTI_NON_CONTEXT_ARRAY) unless contexts.is_a?(Array)
  return create_invalid_context(ERR_KIND_MULTI_WITH_NO_KINDS) if contexts.empty?

  kinds = Set.new
  contexts.each do |context|
    if !context.is_a?(LDContext)
      return create_invalid_context(ERR_KIND_MULTI_NON_CONTEXT_ARRAY)
    elsif !context.valid?
      return create_invalid_context(ERR_KIND_MULTI_NON_CONTEXT_ARRAY)
    elsif context.multi_kind?
      return create_invalid_context(ERR_KIND_MULTI_CANNOT_CONTAIN_MULTI)
    elsif kinds.include? context.kind
      return create_invalid_context(ERR_KIND_MULTI_DUPLICATES)
    end

    kinds.add(context.kind)
  end

  return contexts[0] if contexts.length == 1

  full_key = contexts.sort_by(&:kind)
                     .map { |c| LaunchDarkly::Impl::Context::canonicalize_key_for_kind(c.kind, c.key) }
                     .join(":")

  new(nil, full_key, "multi", nil, false, nil, nil, nil, contexts)
end

.with_key(key, kind = KIND_DEFAULT) ⇒ Object

Convenience method to create a simple single kind context providing only a key and kind type.

Parameters:

  • key (String)
  • kind (String) (defaults to: KIND_DEFAULT)


453
454
455
# File 'lib/ldclient-rb/context.rb', line 453

def self.with_key(key, kind = KIND_DEFAULT)
  create({key: key, kind: kind})
end

Instance Method Details

#==(other) ⇒ Boolean Also known as: eql?

An LDContext can be compared to other LDContexts or to a hash object. If a hash is provided, it is first converted to an LDContext using the LDContext.create method.

Parameters:

Returns:

  • (Boolean)


333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/ldclient-rb/context.rb', line 333

def ==(other)
  other = LDContext.create(other) if other.is_a? Hash
  return false unless other.is_a? LDContext

  return false unless self.kind == other.kind
  return false unless self.valid? == other.valid?
  return false unless self.error == other.error

  return false unless self.individual_context_count == other.individual_context_count

  if self.multi_kind?
    self.kinds.each do |kind|
      return false unless self.individual_context(kind) == other.individual_context(kind)
    end

    return true
  end

  return false unless self.key == other.key
  return false unless self.name == other.name
  return false unless self.anonymous == other.anonymous
  return false unless self.attributes == other.attributes

  # TODO(sc-227265): Calling .to_set is unnecessary once private_attributes are sets.
  return false unless self.private_attributes.to_set == other.private_attributes.to_set

  true
end

#[](key) ⇒ Object

For a single-kind context, the provided key will return the attribute value specified. This is the same as calling LDCotnext.get_value.

For multi-kind contexts, the key will be interpreted as a context kind. If the multi-kind context has an individual context of that kind, it will be returned. Otherwise, this method will return nil. This behaves the same as calling LDContext.individual_context.

Parameters:

  • key (Symbol, String)


373
374
375
376
# File 'lib/ldclient-rb/context.rb', line 373

def [](key)
  return nil unless key.is_a? Symbol or key.is_a? String
  multi_kind? ? individual_context(key.to_s) : get_value(key)
end

#get_custom_attribute_namesArray<Symbol>

Return an array of top level attribute keys (excluding built-in attributes)

The keys come back in the form the caller supplied. A string key is returned as a string, even though the SDK cannot address it.

Returns:

  • (Array<Symbol>)


167
168
169
170
171
# File 'lib/ldclient-rb/context.rb', line 167

def get_custom_attribute_names
  return [] if @attributes.nil?

  @attributes.keys
end

#get_value(attribute) ⇒ any

get_value looks up the value of any attribute of the Context by name. This includes only attributes that are addressable in evaluations-- not metadata such as private attributes.

For a single-kind context, the attribute name can be any custom attribute. It can also be one of the built-in ones like "kind", "key", or "name".

For a multi-kind context, the only supported attribute name is "kind". Use #individual_context to inspect a Context for a particular kind and then get its attributes.

This method does not support complex expressions for getting individual values out of JSON objects or arrays, such as "/address/street". Use #get_value_for_reference for that purpose.

The lookup treats the name as a symbol. An attribute that was stored under a string name is therefore not found, and the result is nil.

If the value is found, the return value is the attribute value; otherwise, it is nil.

Parameters:

  • attribute (String, Symbol)

Returns:

  • (any)


198
199
200
201
# File 'lib/ldclient-rb/context.rb', line 198

def get_value(attribute)
  reference = Reference.create_literal(attribute)
  get_value_for_reference(reference)
end

#get_value_for_reference(reference) ⇒ any

get_value_for_reference looks up the value of any attribute of the Context, or a value contained within an attribute, based on a Reference instance. This includes only attributes that are addressable in evaluations-- not metadata such as private attributes.

This implements the same behavior that the SDK uses to resolve attribute references during a flag evaluation. In a single-kind context, the Reference can represent a simple attribute name-- either a built-in one like "name" or "key", or a custom attribute -- or, it can be a slash-delimited path using a JSON-Pointer-like syntax. See Reference for more details.

For a multi-kind context, the only supported attribute name is "kind". Use #individual_context to inspect a Context for a particular kind and then get its attributes.

A Reference holds its path components as symbols, so each step of the lookup matches a symbol key. A path that crosses an attribute with a string name resolves to nil.

If the value is found, the return value is the attribute value; otherwise, it is nil.

Parameters:

Returns:

  • (any)


230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/ldclient-rb/context.rb', line 230

def get_value_for_reference(reference)
  return nil unless valid?
  return nil unless reference.is_a?(Reference)
  return nil unless reference.error.nil?

  first_component = reference.component(0)
  return nil if first_component.nil?

  if multi_kind?
    if reference.depth == 1 && first_component == :kind
      return kind
    end

    # Multi-kind contexts have no other addressable attributes
    return nil
  end

  value = get_top_level_addressable_attribute_single_kind(first_component)
  return nil if value.nil?

  (1...reference.depth).each do |i|
    name = reference.component(i)

    return nil unless value.is_a?(Hash)
    return nil unless value.has_key?(name)

    value = value[name]
  end

  value
end

#individual_context(kind) ⇒ LDContext?

Returns the single-kind LDContext corresponding to one of the kinds in this context.

The kind parameter can be either a number representing a zero-based index, or a string representing a context kind.

If this method is called on a single-kind LDContext, then the only allowable value for kind is either zero or the same value as #kind, and the return value on success is the same LDContext.

If the method is called on a multi-context, and kind is a number, it must be a non-negative index that is less than the number of kinds (that is, less than the return value of #individual_context_count, and the return value on success is one of the individual LDContexts within. Or, if kind is a string, it must match the context kind of one of the individual contexts.

If there is no context corresponding to kind, the method returns nil.

Parameters:

  • kind (Integer, String)

    the index or string value of a context kind

Returns:

  • (LDContext, nil)

    the context corresponding to that index or kind, or null if none.



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/ldclient-rb/context.rb', line 301

def individual_context(kind)
  return nil unless valid?

  if kind.is_a?(Integer)
    unless multi_kind?
      return kind == 0 ? self : nil
    end

    return kind >= 0 && kind < @contexts.count ? @contexts[kind] : nil
  end

  return nil unless kind.is_a?(String)

  unless multi_kind?
    return self.kind == kind ? self : nil
  end

  @contexts.each do |context|
    return context if context.kind == kind
  end

  nil
end

#individual_context_countInteger

Returns the number of context kinds in this context.

For a valid individual context, this returns 1. For a multi-context, it returns the number of context kinds. For an invalid context, it returns zero.

Returns:

  • (Integer)

    the number of context kinds



271
272
273
274
275
# File 'lib/ldclient-rb/context.rb', line 271

def individual_context_count
  return 0 unless valid?
  return 1 if @contexts.nil?
  @contexts.count
end

#keysHash<Symbol, String>

Returns a hash mapping each context's kind to its key.

Returns:

  • (Hash<Symbol, String>)


140
141
142
143
144
145
# File 'lib/ldclient-rb/context.rb', line 140

def keys
  return {} unless valid?
  return Hash[kind, key] unless multi_kind?

  @contexts.map { |c| [c.kind, c.key] }.to_h
end

#kindsArray<String>

Returns an array of context kinds.

Returns:

  • (Array<String>)


152
153
154
155
156
157
# File 'lib/ldclient-rb/context.rb', line 152

def kinds
  return [] unless valid?
  return [kind] unless multi_kind?

  @contexts.map { |c| c.kind }
end

#multi_kind?Boolean

Returns Is this LDContext a multi-kind context?.

Returns:

  • (Boolean)

    Is this LDContext a multi-kind context?



104
105
106
# File 'lib/ldclient-rb/context.rb', line 104

def multi_kind?
  @is_multi
end

#private_attributesArray<Reference>

Returns the private attributes associated with this LDContext

Returns:

  • (Array<Reference>)

    Returns the private attributes associated with this LDContext



96
97
98
99
# File 'lib/ldclient-rb/context.rb', line 96

def private_attributes
  # TODO(sc-227265): Return a set instead of an array.
  @private_attributes.to_a
end

#to_hHash

Convert the LDContext to a hash. If the LDContext is invalid, the hash will contain an error key with the error message.

Returns:

  • (Hash)


394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/ldclient-rb/context.rb', line 394

def to_h
  return {error: error} unless valid?
  return hash_single_kind unless multi_kind?

  hash = {kind: 'multi'}
  @contexts.each do |context|
    single_kind_hash = context.to_h
    kind = single_kind_hash.delete(:kind)
    hash[kind] = single_kind_hash
  end

  hash
end

#to_json(*args) ⇒ String

Convert the LDContext to a JSON string.

Parameters:

  • args (Array)

Returns:

  • (String)


384
385
386
# File 'lib/ldclient-rb/context.rb', line 384

def to_json(*args)
  JSON.generate(to_h, *args)
end

#valid?Boolean

Returns Determine if this LDContext is considered valid.

Returns:

  • (Boolean)

    Determine if this LDContext is considered valid



111
112
113
# File 'lib/ldclient-rb/context.rb', line 111

def valid?
  @error.nil?
end

#without_anonymous_contextsObject

For a multi-kind context:

A multi-kind context is made up of two or more single-kind contexts. This method will first discard any single-kind contexts which are anonymous. It will then create a new multi-kind context from the remaining single-kind contexts. This may result in an invalid context (e.g. all single-kind contexts are anonymous).

For a single-kind context:

If the context is not anonymous, this method will return the current context as is and unmodified.

If the context is anonymous, this method will return an invalid context.



128
129
130
131
132
133
# File 'lib/ldclient-rb/context.rb', line 128

def without_anonymous_contexts
  contexts = multi_kind? ? @contexts : [self]
  contexts = contexts.reject { |c| c.anonymous }

  LDContext.create_multi(contexts)
end