Skip to content

Commit

Permalink
fix: LDContext equality is no longer order dependent (#265)
Browse files Browse the repository at this point in the history
  • Loading branch information
keelerm84 committed Dec 29, 2023
1 parent 66c4f2d commit 683e0c3
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 10 deletions.
48 changes: 47 additions & 1 deletion ContractTests/Source/Controllers/SdkController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ final class SdkController: RouteCollection {
"service-endpoints",
"strongly-typed",
"tags",
"user-type"
"user-type",
"context-comparison"
]

return StatusResponse(
Expand Down Expand Up @@ -220,13 +221,58 @@ final class SdkController: RouteCollection {
let response = ContextBuildResponse(output: nil, error: error.localizedDescription)
return CommandResponse.contextBuild(response)
}
case "contextComparison":
let params = commandParameters.contextComparison!
let context1 = try SdkController.buildContextForComparison(params.context1)
let context2 = try SdkController.buildContextForComparison(params.context2)

let response = ContextComparisonResponse(equals: context1 == context2)
return CommandResponse.contextComparison(response)
default:
throw Abort(.badRequest)
}

return CommandResponse.ok
}

static func buildContextForComparison(_ params: ContextComparisonParameters) throws -> LDContext {
if let single = params.single {
return try buildSingleKindContextForComparison(single)
} else if let multi = params.multi {
var builder = LDMultiContextBuilder()
for param in multi {
builder.addContext(try buildSingleKindContextForComparison(param))
}

return try builder.build().get()
}

throw Abort(.badRequest)
}

static func buildSingleKindContextForComparison(_ params: ContextComparisonSingleParams) throws -> LDContext {
var builder = LDContextBuilder(key: params.key)
builder.kind(params.kind)

if let attributes = params.attributes {
for attribute in attributes {
builder.trySetValue(attribute.name, attribute.value)
}
}

if let attributes = params.privateAttributes {
for attribute in attributes {
if attribute.literal {
builder.addPrivateAttribute(Reference(literal: attribute.value))
} else {
builder.addPrivateAttribute(Reference(attribute.value))
}
}
}

return try builder.build().get()
}

static func buildSingleContextFromParams(_ params: SingleContextParameters) throws -> LDContext {
var contextBuilder = LDContextBuilder(key: params.key)

Expand Down
36 changes: 36 additions & 0 deletions ContractTests/Source/Models/command.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ enum CommandResponse: Content, Encodable {
case evaluateAll(EvaluateAllFlagsResponse)
case contextBuild(ContextBuildResponse)
case contextConvert(ContextBuildResponse)
case contextComparison(ContextComparisonResponse)
case ok

func encode(to encoder: Encoder) throws {
Expand All @@ -24,6 +25,9 @@ enum CommandResponse: Content, Encodable {
case .contextConvert(let response):
try container.encode(response)
return
case .contextComparison(let response):
try container.encode(response)
return
case .ok:
try container.encode(true)
return
Expand All @@ -39,6 +43,7 @@ struct CommandParameters: Content {
var identifyEvent: IdentifyEventParameters?
var contextBuild: ContextBuildParameters?
var contextConvert: ContextConvertParameters?
var contextComparison: ContextComparisonPairParameters?
}

struct EvaluateFlagParameters: Content {
Expand Down Expand Up @@ -100,3 +105,34 @@ struct ContextBuildResponse: Content, Encodable {
struct ContextConvertParameters: Content, Decodable {
var input: String
}

struct ContextComparisonPairParameters: Content, Decodable {
var context1: ContextComparisonParameters
var context2: ContextComparisonParameters
}

struct ContextComparisonParameters: Content, Decodable {
var single: ContextComparisonSingleParams?
var multi: [ContextComparisonSingleParams]?
}

struct ContextComparisonSingleParams: Content, Decodable {
var kind: String
var key: String
var attributes: [AttributeDefinition]?
var privateAttributes: [PrivateAttribute]?
}

struct AttributeDefinition: Content, Decodable {
var name: String
var value: LDValue
}

struct PrivateAttribute: Content, Decodable {
var value: String
var literal: Bool
}

struct ContextComparisonResponse: Content, Encodable {
var equals: Bool
}
14 changes: 7 additions & 7 deletions LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public struct LDContext: Encodable, Equatable {
// Meta attributes
fileprivate var name: String?
fileprivate var anonymous: Bool = false
internal var privateAttributes: [Reference] = []
internal var privateAttributes: Set<Reference> = []

fileprivate var key: String?
fileprivate var canonicalizedKey: String
Expand All @@ -46,7 +46,7 @@ public struct LDContext: Encodable, Equatable {
}

struct Meta: Codable {
var privateAttributes: [Reference]?
var privateAttributes: Set<Reference>?
var redactedAttributes: [String]?

enum CodingKeys: CodingKey {
Expand All @@ -58,15 +58,15 @@ public struct LDContext: Encodable, Equatable {
&& (redactedAttributes?.isEmpty ?? true)
}

init(privateAttributes: [Reference]?, redactedAttributes: [String]?) {
init(privateAttributes: Set<Reference>?, redactedAttributes: [String]?) {
self.privateAttributes = privateAttributes
self.redactedAttributes = redactedAttributes
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

let privateAttributes = try container.decodeIfPresent([Reference].self, forKey: .privateAttributes)
let privateAttributes = try container.decodeIfPresent(Set<Reference>.self, forKey: .privateAttributes)

self.privateAttributes = privateAttributes
self.redactedAttributes = []
Expand Down Expand Up @@ -593,7 +593,7 @@ public struct LDContextBuilder {
// Meta attributes
private var name: String?
private var anonymous: Bool = false
private var privateAttributes: [Reference] = []
private var privateAttributes: Set<Reference> = []

private var key: LDContextBuilderKey
private var attributes: [String: LDValue] = [:]
Expand Down Expand Up @@ -782,13 +782,13 @@ public struct LDContextBuilder {
/// If an attribute's actual name starts with a '/' character, you must use the same escaping syntax as
/// JSON Pointer: replace "~" with "~0", and "/" with "~1".
public mutating func addPrivateAttribute(_ reference: Reference) {
self.privateAttributes.append(reference)
self.privateAttributes.insert(reference)
}

/// Remove any reference provided through `addPrivateAttribute(_:)`. If the reference was
/// added more than once, this method will remove all instances of it.
public mutating func removePrivateAttribute(_ reference: Reference) {
self.privateAttributes.removeAll { $0 == reference }
self.privateAttributes.remove(reference)
}

/// Creates a LDContext from the current LDContextBuilder properties.
Expand Down
9 changes: 8 additions & 1 deletion LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ extension ReferenceError: CustomStringConvertible {
/// - Reference("name") or Reference("/name") would refer to the value "xyz"
/// - Reference("/address/street") would refer to the value "99 Main St."
/// - Reference("a/b") or Reference("/a~1b") would refer to the value "ok"
public struct Reference: Codable, Hashable {
public struct Reference: Codable {
private var error: ReferenceError?
private var rawPath: String
private var components: [String] = []
Expand Down Expand Up @@ -236,3 +236,10 @@ extension Reference: Equatable {
return lhs.error == rhs.error && lhs.components == rhs.components
}
}

extension Reference: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(error)
hasher.combine(components)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ import XCTest
@testable import LaunchDarkly

final class LDContextSpec: XCTestCase {
func testContextsAreEquatable() throws {
func testSingleKindContextsAreEqual() throws {
var originalBuilder = LDContextBuilder(key: "context-key")
originalBuilder.kind("user")
originalBuilder.name("Example name")
originalBuilder.trySetValue("groups", LDValue.array(["test", "it", "here"]))
originalBuilder.trySetValue("address", LDValue.object(["address": "123 Easy St", "city": "Every Town"]))
originalBuilder.addPrivateAttribute(Reference(literal: "name"))
originalBuilder.addPrivateAttribute(Reference("out of order attribute"))

var duplicateBuilder = LDContextBuilder(key: "context-key")
duplicateBuilder.kind("user")
duplicateBuilder.name("Example name")
duplicateBuilder.trySetValue("groups", LDValue.array(["test", "it", "here"]))
duplicateBuilder.trySetValue("address", LDValue.object(["address": "123 Easy St", "city": "Every Town"]))
duplicateBuilder.addPrivateAttribute(Reference("out of order attribute"))
duplicateBuilder.addPrivateAttribute(Reference("/name"))

let original = try originalBuilder.build().get()
Expand All @@ -25,6 +27,54 @@ final class LDContextSpec: XCTestCase {
XCTAssertEqual(original, duplicate)
}

func testMultiKindContextsAreEqual() throws {
var orgBuilder = LDContextBuilder(key: "org-key")
orgBuilder.kind("org")

var userBuilder = LDContextBuilder(key: "user-key")
userBuilder.kind("user")

var deviceBuilder = LDContextBuilder(key: "device-key")
deviceBuilder.kind("device")

var originalMultiBuilder = LDMultiContextBuilder()
originalMultiBuilder.addContext(try orgBuilder.build().get())
originalMultiBuilder.addContext(try userBuilder.build().get())

var duplicateMultiBuilder = LDMultiContextBuilder()
duplicateMultiBuilder.addContext(try userBuilder.build().get())
duplicateMultiBuilder.addContext(try orgBuilder.build().get())

let original = try originalMultiBuilder.build().get()
let duplicate = try duplicateMultiBuilder.build().get()

XCTAssertEqual(original, duplicate)

duplicateMultiBuilder.addContext(try deviceBuilder.build().get())
let updatedDuplicate = try duplicateMultiBuilder.build().get()

XCTAssertNotEqual(original, updatedDuplicate)
}

func testMultiAndSingleAreNotEqual() throws {
var orgBuilder = LDContextBuilder(key: "org-key")
orgBuilder.kind("org")

var userBuilder = LDContextBuilder(key: "user-key")
userBuilder.kind("user")

var multiBuilder = LDMultiContextBuilder()
multiBuilder.addContext(try orgBuilder.build().get())

// These should be the same as a single kind multi-context gets flattened
let multi = try multiBuilder.build().get()
XCTAssertEqual(multi, try orgBuilder.build().get())

multiBuilder.addContext(try userBuilder.build().get())
let updatedMulti = try multiBuilder.build().get()
XCTAssertNotEqual(updatedMulti, try orgBuilder.build().get())
}

func testContextsAreNotTheSame() throws {
let values: [String: LDValue] = [
"kind": "org",
Expand Down

0 comments on commit 683e0c3

Please sign in to comment.