Skip to content

Commit 1b2ba7b

Browse files
authored
feat: Define equality methods for LDContext and Reference (#232)
1 parent 8366e57 commit 1b2ba7b

File tree

7 files changed

+260
-91
lines changed

7 files changed

+260
-91
lines changed

contract-tests/client_entity.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,45 @@ def migration_operation(params)
144144
end
145145
end
146146

147+
def context_comparison(params)
148+
context1 = build_context_from_params(params[:context1])
149+
context2 = build_context_from_params(params[:context2])
150+
151+
context1 == context2
152+
end
153+
154+
private def build_context_from_params(params)
155+
return build_single_context_from_attribute_definitions(params[:single]) unless params[:single].nil?
156+
157+
contexts = params[:multi].map do |param|
158+
build_single_context_from_attribute_definitions(param)
159+
end
160+
161+
LaunchDarkly::LDContext.create_multi(contexts)
162+
end
163+
164+
private def build_single_context_from_attribute_definitions(params)
165+
context = {kind: params[:kind], key: params[:key]}
166+
167+
params[:attributes]&.each do |attribute|
168+
context[attribute[:name]] = attribute[:value]
169+
end
170+
171+
if params[:privateAttributes]
172+
context[:_meta] = {
173+
privateAttributes: params[:privateAttributes].map do |attribute|
174+
if attribute[:literal]
175+
LaunchDarkly::Reference.create_literal(attribute[:value])
176+
else
177+
LaunchDarkly::Reference.create(attribute[:value])
178+
end
179+
end,
180+
}
181+
end
182+
183+
LaunchDarkly::LDContext.create(context)
184+
end
185+
147186
def secure_mode_hash(params)
148187
@client.secure_mode_hash(params[:context])
149188
end

contract-tests/service.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
'tags',
3636
'migrations',
3737
'event-sampling',
38+
'context-comparison',
3839
],
3940
}.to_json
4041
end
@@ -109,6 +110,9 @@
109110
when "migrationOperation"
110111
response = {:result => client.migration_operation(params[:migrationOperation]).to_s}
111112
return [200, nil, response.to_json]
113+
when "contextComparison"
114+
response = {:equals => client.context_comparison(params[:contextComparison])}
115+
return [200, nil, response.to_json]
112116
end
113117

114118
return [400, nil, {:error => "Unknown command requested"}.to_json]

lib/ldclient-rb/context.rb

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,6 @@ class LDContext
4747
# @return [String, nil] Returns the error associated with this LDContext if invalid
4848
attr_reader :error
4949

50-
# @return [Array<Reference>] Returns the private attributes associated with this LDContext
51-
attr_reader :private_attributes
52-
5350
#
5451
# @private
5552
# @param key [String, nil]
@@ -69,17 +66,27 @@ def initialize(key, fully_qualified_key, kind, name = nil, anonymous = nil, attr
6966
@name = name
7067
@anonymous = anonymous || false
7168
@attributes = attributes
72-
@private_attributes = []
69+
@private_attributes = Set.new
7370
(private_attributes || []).each do |attribute|
7471
reference = Reference.create(attribute)
75-
@private_attributes << reference if reference.error.nil?
72+
@private_attributes.add(reference) if reference.error.nil?
7673
end
7774
@error = error
7875
@contexts = contexts
7976
@is_multi = !contexts.nil?
8077
end
8178
private_class_method :new
8279

80+
protected attr_reader :name, :anonymous, :attributes
81+
82+
#
83+
# @return [Array<Reference>] Returns the private attributes associated with this LDContext
84+
#
85+
def private_attributes
86+
# TODO(sc-227265): Return a set instead of an array.
87+
@private_attributes.to_a
88+
end
89+
8390
#
8491
# @return [Boolean] Is this LDContext a multi-kind context?
8592
#
@@ -274,6 +281,33 @@ def individual_context(kind)
274281
nil
275282
end
276283

284+
def ==(other)
285+
return false unless self.kind == other.kind
286+
return false unless self.valid? == other.valid?
287+
return false unless self.error == other.error
288+
289+
return false unless self.individual_context_count == other.individual_context_count
290+
291+
if self.multi_kind?
292+
self.kinds.each do |kind|
293+
return false unless self.individual_context(kind) == other.individual_context(kind)
294+
end
295+
296+
return true
297+
end
298+
299+
return false unless self.key == other.key
300+
return false unless self.name == other.name
301+
return false unless self.anonymous == other.anonymous
302+
return false unless self.attributes == other.attributes
303+
304+
# TODO(sc-227265): Calling .to_set is unnecessary once private_attributes are sets.
305+
return false unless self.private_attributes.to_set == other.private_attributes.to_set
306+
307+
true
308+
end
309+
alias eql? ==
310+
277311
#
278312
# Retrieve the value of any top level, addressable attribute.
279313
#

lib/ldclient-rb/impl/context_filter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,4 @@ def filter(context)
142142
end
143143
end
144144
end
145-
end
145+
end

lib/ldclient-rb/reference.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ def initialize(raw_path, components = [], error = nil)
109109
end
110110
private_class_method :new
111111

112+
protected attr_reader :components
113+
112114
#
113115
# Creates a Reference from a string. For the supported syntax and examples,
114116
# see comments on the Reference type.
@@ -227,6 +229,15 @@ def component(index)
227229
@components[index]
228230
end
229231

232+
def ==(other)
233+
self.error == other.error && self.components == other.components
234+
end
235+
alias eql? ==
236+
237+
def hash
238+
([error] + components).hash
239+
end
240+
230241
#
231242
# Performs unescaping of attribute reference path components:
232243
#

spec/context_spec.rb

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,5 +296,68 @@ module LaunchDarkly
296296
end
297297
end
298298
end
299+
300+
describe "equality comparisons" do
301+
it "single kind contexts are equal" do
302+
original_context = subject.create(
303+
{ key: 'context-key', kind: 'user', name: 'Example name', groups: ['test', 'it', 'here'], address: {street: '123 Easy St', city: 'Every Town'},
304+
_meta: { privateAttributes: ['name', 'out of order attribute'] }
305+
})
306+
duplicate_context = subject.create(
307+
{ key: 'context-key', kind: 'user', name: 'Example name', groups: ['test', 'it', 'here'], address: {street: '123 Easy St', city: 'Every Town'},
308+
_meta: { privateAttributes: ['out of order attribute', 'name'] }
309+
})
310+
expect(original_context).to eq(duplicate_context)
311+
end
312+
313+
it "multi kind contexts are equal" do
314+
org_context = subject.create({ key: 'org-key', kind: 'org' })
315+
user_context = subject.create({ key: 'user-key', kind: 'user' })
316+
device_context = subject.create({ key: 'device-key', kind: 'device' })
317+
318+
original_context = subject.create_multi([org_context, user_context])
319+
duplicate_context = subject.create_multi([user_context, org_context])
320+
321+
expect(original_context).to eq(duplicate_context)
322+
323+
superset_context = subject.create_multi([org_context, user_context, device_context])
324+
expect(superset_context).not_to eq(duplicate_context)
325+
end
326+
327+
it "mixed size contexts are not equal" do
328+
org_context = subject.create({ key: 'org-key', kind: 'org' })
329+
user_context = subject.create({ key: 'user-key', kind: 'user' })
330+
331+
flattened_multi = subject.create_multi([org_context])
332+
333+
expect(flattened_multi).to eq(org_context)
334+
335+
multi = subject.create_multi([org_context, user_context])
336+
expect(multi).not_to eq(org_context)
337+
expect(multi).not_to eq(user_context)
338+
end
339+
340+
it "failed contexts can be equal" do
341+
invalid_hash = subject.create(true)
342+
invalid_kind = subject.create({ kind: 'this is not valid' })
343+
invalid_key = subject.create({ key: nil })
344+
invalid_name = subject.create({ key: 'user-key', name: true })
345+
invalid_anonymous = subject.create({ key: 'user-key', anonymous: 'this is no boolean' })
346+
invalid_private_attributes = subject.create({ key: 'user-key', _meta: { privateAttributes: 'this is no array' }})
347+
348+
expect(invalid_hash).not_to eq(invalid_kind)
349+
expect(invalid_hash).not_to eq(invalid_key)
350+
expect(invalid_hash).not_to eq(invalid_name)
351+
expect(invalid_hash).not_to eq(invalid_anonymous)
352+
expect(invalid_hash).not_to eq(invalid_private_attributes)
353+
354+
expect(invalid_hash).to eq(subject.create(true))
355+
expect(invalid_kind).to eq(subject.create({ kind: 'this is not valid' }))
356+
expect(invalid_key).to eq(subject.create({ key: nil }))
357+
expect(invalid_name).to eq(subject.create({ key: 'user-key', name: true }))
358+
expect(invalid_anonymous).to eq(subject.create({ key: 'user-key', anonymous: 'this is no boolean' }))
359+
expect(invalid_private_attributes).to eq(subject.create({ key: 'user-key', _meta: { privateAttributes: 'this is no array' }}))
360+
end
361+
end
299362
end
300363
end

0 commit comments

Comments
 (0)