diff --git a/lib/ldclient-rb/config.rb b/lib/ldclient-rb/config.rb index 1b77ac16..bc116471 100644 --- a/lib/ldclient-rb/config.rb +++ b/lib/ldclient-rb/config.rb @@ -34,7 +34,7 @@ class Config # @option opts [Float] :poll_interval (30) See {#poll_interval}. # @option opts [Boolean] :stream (true) See {#stream?}. # @option opts [Boolean] all_attributes_private (false) See {#all_attributes_private}. - # @option opts [Array] :private_attributes See {#private_attributes}. + # @option opts [Enumerable] :private_attributes See {#private_attributes}. # @option opts [Boolean] :send_events (true) See {#send_events}. # @option opts [Integer] :context_keys_capacity (1000) See {#context_keys_capacity}. # @option opts [Float] :context_keys_flush_interval (300) See {#context_keys_flush_interval}. @@ -69,7 +69,7 @@ def initialize(opts = {}) @offline = opts.has_key?(:offline) ? opts[:offline] : Config.default_offline @poll_interval = opts.has_key?(:poll_interval) && opts[:poll_interval] > Config.default_poll_interval ? opts[:poll_interval] : Config.default_poll_interval @all_attributes_private = opts[:all_attributes_private] || false - @private_attributes = opts[:private_attributes] || [] + @private_attributes = Set.new(opts[:private_attributes] || []).freeze @send_events = opts.has_key?(:send_events) ? opts[:send_events] : Config.default_send_events @compress_events = opts.has_key?(:compress_events) ? opts[:compress_events] : Config.default_compress_events @context_keys_capacity = opts[:context_keys_capacity] || Config.default_context_keys_capacity @@ -255,7 +255,7 @@ def offline? attr_reader :all_attributes_private # - # A list of context attribute names that should always be considered private. This means that the + # A set of context attribute names that should always be considered private. This means that the # attribute values will not be sent to LaunchDarkly in analytics events and will not appear on # the LaunchDarkly dashboard. # @@ -268,7 +268,7 @@ def offline? # # @see https://docs.launchdarkly.com/sdk/features/user-context-config#using-private-attributes # - # @return [Array] + # @return [Set] # @see #all_attributes_private # attr_reader :private_attributes diff --git a/lib/ldclient-rb/context.rb b/lib/ldclient-rb/context.rb index 58e8c40a..616e4512 100644 --- a/lib/ldclient-rb/context.rb +++ b/lib/ldclient-rb/context.rb @@ -44,7 +44,7 @@ class LDContext private_constant :ERR_KIND_MULTI_DUPLICATES ERR_CUSTOM_NON_HASH = 'context custom must be a hash' private_constant :ERR_CUSTOM_NON_HASH - ERR_PRIVATE_NON_ARRAY = 'context private attributes must be an array' + ERR_PRIVATE_NON_ARRAY = 'context private attributes must be an array or a set' # @return [String, nil] Returns the key for this context attr_reader :key @@ -66,7 +66,7 @@ class LDContext # @param name [String, nil] # @param anonymous [Boolean, nil] # @param attributes [Hash, nil] - # @param private_attributes [Array, nil] + # @param private_attributes [Array, Set, nil] # @param error [String, nil] # @param contexts [Array, nil] # @@ -82,6 +82,7 @@ def initialize(key, fully_qualified_key, kind, name = nil, anonymous = nil, attr reference = Reference.create(attribute) @private_attributes.add(reference) if reference.error.nil? end + @private_attributes.freeze @error = error @contexts = contexts @is_multi = !contexts.nil? @@ -91,12 +92,9 @@ def initialize(key, fully_qualified_key, kind, name = nil, anonymous = nil, attr protected attr_reader :name, :anonymous, :attributes # - # @return [Array] Returns the private attributes associated with this LDContext + # @return [Set] Returns the private attributes associated with this LDContext. The set is frozen. # - def private_attributes - # TODO(sc-227265): Return a set instead of an array. - @private_attributes.to_a - end + attr_reader :private_attributes # # @return [Boolean] Is this LDContext a multi-kind context? @@ -353,8 +351,7 @@ def ==(other) 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 + return false unless self.private_attributes == other.private_attributes true end @@ -413,7 +410,7 @@ def to_h hash[:name] = name unless name.nil? hash[:anonymous] = anonymous if anonymous - hash[:_meta] = {privateAttributes: private_attributes} unless private_attributes.empty? + hash[:_meta] = {privateAttributes: private_attributes.to_a} unless private_attributes.empty? hash end @@ -570,7 +567,7 @@ def self.create_multi(contexts) meta = data.fetch(:_meta, {}) private_attributes = meta[:privateAttributes] - if private_attributes && !private_attributes.is_a?(Array) + if private_attributes && !private_attributes.is_a?(Array) && !private_attributes.is_a?(Set) return create_invalid_context(ERR_PRIVATE_NON_ARRAY) end diff --git a/lib/ldclient-rb/impl/context_filter.rb b/lib/ldclient-rb/impl/context_filter.rb index 30c6ecdb..736611f0 100644 --- a/lib/ldclient-rb/impl/context_filter.rb +++ b/lib/ldclient-rb/impl/context_filter.rb @@ -5,7 +5,7 @@ module Impl class ContextFilter # # @param all_attributes_private [Boolean] - # @param private_attributes [Array] + # @param private_attributes [Enumerable] # @param logger [Logger, nil] # def initialize(all_attributes_private, private_attributes, logger = nil) @@ -13,7 +13,7 @@ def initialize(all_attributes_private, private_attributes, logger = nil) @logger = logger @non_symbol_name_logged = Concurrent::AtomicBoolean.new(false) - @private_attributes = [] + @private_attributes = Set.new private_attributes.each do |attribute| reference = LaunchDarkly::Reference.create(attribute) @private_attributes << reference if reference.error.nil? @@ -75,7 +75,7 @@ def filter_redact_anonymous(context) filtered[:anonymous] = true if anonymous redacted = [] - private_attributes = @private_attributes + context.private_attributes + private_attributes = @private_attributes | context.private_attributes name = context.get_value(:name) if !name.nil? && !check_whole_attribute_private(Reference.create_literal(:name), private_attributes, redacted, anonymous && redact_anonymous) @@ -103,7 +103,7 @@ def filter_redact_anonymous(context) # Check if an entire attribute should be redacted. # # @param attribute [Reference] - # @param private_attributes [Array] + # @param private_attributes [Set] # @param redacted [Array] # @param redact_all [Boolean] # @return [Boolean] @@ -130,7 +130,7 @@ def filter_redact_anonymous(context) # @param parent_path [Array, nil] # @param name [String] # @param value [any] - # @param private_attributes [Array] + # @param private_attributes [Set] # @param redacted [Array] # @param visited [Array] # @return [any] diff --git a/lib/ldclient-rb/reference.rb b/lib/ldclient-rb/reference.rb index d50b1e6d..3c93bdaf 100644 --- a/lib/ldclient-rb/reference.rb +++ b/lib/ldclient-rb/reference.rb @@ -125,10 +125,14 @@ def initialize(raw_path, components = [], error = nil) # validation fails, {#error} will return a non-nil error and any SDK method # that takes this Reference as a parameter will consider it invalid. # - # @param value [String, Symbol] + # If the value is a Reference, this method returns the same Reference. + # + # @param value [String, Symbol, Reference] # @return [Reference] # def self.create(value) + return value if value.is_a?(Reference) + unless value.is_a?(String) || value.is_a?(Symbol) return new(value, [], ERR_EMPTY) end diff --git a/spec/config_spec.rb b/spec/config_spec.rb index 687b9b09..eb11a3de 100644 --- a/spec/config_spec.rb +++ b/spec/config_spec.rb @@ -135,5 +135,16 @@ module LaunchDarkly expect(subject.new(omit_anonymous_contexts: true).omit_anonymous_contexts).to eq true end end + describe ".private_attributes" do + it "defaults to an empty set" do + expect(subject.new.private_attributes).to eq Set.new + end + it "converts an array to a set" do + expect(subject.new(private_attributes: ["name", "email", "name"]).private_attributes).to eq Set["name", "email"] + end + it "is frozen" do + expect(subject.new(private_attributes: ["name"]).private_attributes).to be_frozen + end + end end end diff --git a/spec/context_spec.rb b/spec/context_spec.rb index 44be8e4a..f4a65006 100644 --- a/spec/context_spec.rb +++ b/spec/context_spec.rb @@ -77,6 +77,19 @@ module LaunchDarkly expect(subject.create(context).valid?).to be false end + it "accepts privateAttributes as a set" do + context = subject.create({ key: "user-key", kind: "user", _meta: { privateAttributes: Set["email"] } }) + expect(context.valid?).to be true + expect(context.private_attributes).to eq(Set[LaunchDarkly::Reference.create("email")]) + end + + it "accepts privateAttributes from another context" do + original = subject.create({ key: "user-key", kind: "user", _meta: { privateAttributes: ["email", "/address/city"] } }) + context = subject.create({ key: "user-key", kind: "user", _meta: { privateAttributes: original.private_attributes } }) + expect(context.valid?).to be true + expect(context.private_attributes).to eq(original.private_attributes) + end + it "creates the correct fully qualified key" do expect(subject.create({ key: "user-key", kind: "user" }).fully_qualified_key).to eq("user-key") expect(subject.create({ key: "org-key", kind: "org" }).fully_qualified_key).to eq("org:org-key") @@ -394,6 +407,19 @@ module LaunchDarkly end end + describe "private attributes" do + it "returns a frozen set of references" do + context = subject.create({ key: 'context-key', kind: 'user', _meta: { privateAttributes: ['name', 'address/city', 'name'] } }) + expect(context.private_attributes).to be_a(Set) + expect(context.private_attributes).to be_frozen + expect(context.private_attributes).to eq(Set[LaunchDarkly::Reference.create('name'), LaunchDarkly::Reference.create('address/city')]) + end + + it "returns an empty set when none are given" do + expect(subject.create({ key: 'context-key', kind: 'user' }).private_attributes).to eq(Set.new) + end + end + describe "equality comparisons" do it "wrong types are not equal" do context = subject.create({ key: 'context-key', kind: 'user' }) diff --git a/spec/reference_spec.rb b/spec/reference_spec.rb index 4c0d0a51..a4a661f7 100644 --- a/spec/reference_spec.rb +++ b/spec/reference_spec.rb @@ -90,6 +90,11 @@ module LaunchDarkly end end + it "returns the same reference when given a reference" do + reference = Reference.create("/a/b") + expect(Reference.create(reference)).to be(reference) + end + describe "creating literal references" do it "can create valid references" do [