diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 6fb8d06d..4dcab792 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -329,7 +329,7 @@ public class LDClient { public var allFlags: [LDFlagKey: Any]? { guard hasStarted else { return nil } - return flagStore.featureFlags.allFlagValues + return flagStore.featureFlags.compactMapValues { $0.value } } // MARK: Observing Updates diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 75420154..10257a42 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -118,46 +118,6 @@ public struct LDUser: Encodable { return custom[attribute.name] } - /// Dictionary with LDUser attribute keys and values, with options to include feature flags and private attributes. LDConfig object used to help resolving what attributes should be private. - /// - parameter includePrivateAttributes: Controls whether the resulting dictionary includes private attributes - /// - parameter config: Provides supporting information for defining private attributes - func dictionaryValue(includePrivateAttributes includePrivate: Bool, config: LDConfig) -> [String: Any] { - let allPrivate = !includePrivate && config.allUserAttributesPrivate - let privateAttributeNames = includePrivate ? [] : (privateAttributes + config.privateUserAttributes).map { $0.name } - - var dictionary: [String: Any] = [:] - var redactedAttributes: [String] = [] - - dictionary[CodingKeys.key.rawValue] = key - dictionary[CodingKeys.isAnonymous.rawValue] = isAnonymous - - LDUser.optionalAttributes.forEach { attribute in - if let value = self.value(for: attribute) { - if allPrivate || privateAttributeNames.contains(attribute.name) { - redactedAttributes.append(attribute.name) - } else { - dictionary[attribute.name] = value - } - } - } - - var customDictionary: [String: Any] = [:] - custom.forEach { attrName, attrVal in - if allPrivate || privateAttributeNames.contains(attrName) { - redactedAttributes.append(attrName) - } else { - customDictionary[attrName] = attrVal.toAny() - } - } - dictionary[CodingKeys.custom.rawValue] = customDictionary.isEmpty ? nil : customDictionary - - if !redactedAttributes.isEmpty { - dictionary[CodingKeys.privateAttributes.rawValue] = Set(redactedAttributes).sorted() - } - - return dictionary - } - struct UserInfoKeys { static let includePrivateAttributes = CodingUserInfoKey(rawValue: "LD_includePrivateAttributes")! static let allAttributesPrivate = CodingUserInfoKey(rawValue: "LD_allAttributesPrivate")! @@ -246,23 +206,3 @@ extension LDUserWrapper { } extension LDUser: TypeIdentifying { } - -#if DEBUG - extension LDUser { - // Compares all user properties. - func isEqual(to otherUser: LDUser) -> Bool { - key == otherUser.key - && secondary == otherUser.secondary - && name == otherUser.name - && firstName == otherUser.firstName - && lastName == otherUser.lastName - && country == otherUser.country - && ipAddress == otherUser.ipAddress - && email == otherUser.email - && avatar == otherUser.avatar - && custom == otherUser.custom - && isAnonymous == otherUser.isAnonymous - && privateAttributes == otherUser.privateAttributes - } - } -#endif diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift index b9668f05..e952edfb 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DeprecatedCache.swift @@ -42,11 +42,3 @@ extension Dictionary where Key == String, Value == Any { (self[LDUser.CodingKeys.lastUpdated] as? String)?.dateValue } } - -#if DEBUG -extension Dictionary where Key == String, Value == Any { - mutating func setLastUpdated(_ lastUpdated: Date?) { - self[LDUser.CodingKeys.lastUpdated] = lastUpdated?.stringValue - } -} -#endif diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift index b18ab10e..ac9ed9e2 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift @@ -127,7 +127,3 @@ final class FlagStore: FlagMaintaining { } extension FlagStore: TypeIdentifying { } - -extension Dictionary where Key == LDFlagKey, Value == FeatureFlag { - var allFlagValues: [LDFlagKey: Any] { compactMapValues { $0.value } } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift index 7bf06860..13797e2c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift @@ -7,8 +7,6 @@ final class LDUserSpec: QuickSpec { override func spec() { initSpec() - dictionaryValueSpec() - isEqualSpec() } private func initSpec() { @@ -115,254 +113,4 @@ final class LDUserSpec: QuickSpec { } } } - - private func dictionaryValueSpec() { - let optionalNames = LDUser.optionalAttributes.map { $0.name } - let allCustomPrivitizable = Array(LDUser.StubConstants.custom(includeSystemValues: true).keys) - - describe("dictionaryValue") { - var user: LDUser! - var config: LDConfig! - var userDictionary: [String: Any]! - - beforeEach { - config = LDConfig.stub - user = LDUser.stub() - } - - context("with an empty user") { - beforeEach { - user = LDUser() - // Remove SDK set attributes - user.custom = [:] - } - // Should be the same regardless of including/privitizing attributes - let testCase = { - it("creates expected user dictionary") { - expect(userDictionary.count) == 2 - // Required attributes - expect(userDictionary[LDUser.CodingKeys.key.rawValue] as? String) == user.key - expect(userDictionary[LDUser.CodingKeys.isAnonymous.rawValue] as? Bool) == user.isAnonymous - } - } - context("including private attributes") { - beforeEach { - userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - } - testCase() - } - context("privatizing all globally") { - beforeEach { - config.allUserAttributesPrivate = true - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - testCase() - } - context("privatizing all individually in config") { - beforeEach { - config.privateUserAttributes = LDUser.optionalAttributes + [UserAttribute.forName("customAttr")] - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - testCase() - } - context("privatizing all individually on user") { - beforeEach { - user.privateAttributes = LDUser.optionalAttributes + [UserAttribute.forName("customAttr")] - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - testCase() - } - } - - it("includePrivateAttributes always includes attributes") { - config.allUserAttributesPrivate = true - config.privateUserAttributes = LDUser.optionalAttributes + allCustomPrivitizable.map { UserAttribute.forName($0) } - user.privateAttributes = LDUser.optionalAttributes + allCustomPrivitizable.map { UserAttribute.forName($0) } - let userDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - - expect(userDictionary.count) == 11 - - // Required attributes - expect(userDictionary[LDUser.CodingKeys.key.rawValue] as? String) == user.key - expect(userDictionary[LDUser.CodingKeys.isAnonymous.rawValue] as? Bool) == user.isAnonymous - - // Built-in optional attributes - expect(userDictionary[LDUser.CodingKeys.name.rawValue] as? String) == user.name - expect(userDictionary[LDUser.CodingKeys.firstName.rawValue] as? String) == user.firstName - expect(userDictionary[LDUser.CodingKeys.lastName.rawValue] as? String) == user.lastName - expect(userDictionary[LDUser.CodingKeys.email.rawValue] as? String) == user.email - expect(userDictionary[LDUser.CodingKeys.ipAddress.rawValue] as? String) == user.ipAddress - expect(userDictionary[LDUser.CodingKeys.avatar.rawValue] as? String) == user.avatar - expect(userDictionary[LDUser.CodingKeys.secondary.rawValue] as? String) == user.secondary - expect(userDictionary[LDUser.CodingKeys.country.rawValue] as? String) == user.country - - let customDictionary = userDictionary.customDictionary()! - expect(customDictionary.count) == allCustomPrivitizable.count - - // Custom attributes - allCustomPrivitizable.forEach { attr in - expect(LDValue.fromAny(customDictionary[attr])) == user.custom[attr] - } - - // Redacted attributes is empty - expect(userDictionary[LDUser.CodingKeys.privateAttributes.rawValue]).to(beNil()) - } - - [false, true].forEach { isCustomAttr in - (isCustomAttr ? LDUser.StubConstants.custom(includeSystemValues: true).keys.map { UserAttribute.forName($0) } - : LDUser.optionalAttributes).forEach { privateAttr in - [false, true].forEach { inConfig in - it("with \(privateAttr) private in \(inConfig ? "config" : "user")") { - if inConfig { - config.privateUserAttributes = [privateAttr] - } else { - user.privateAttributes = [privateAttr] - } - - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - - expect(userDictionary.redactedAttributes) == [privateAttr.name] - - let includingDictionary = user.dictionaryValue(includePrivateAttributes: true, config: config) - if !isCustomAttr { - let userDictionaryWithoutRedacted = userDictionary.filter { $0.key != "privateAttrs" } - let includingDictionaryWithoutRedacted = includingDictionary.filter { $0.key != privateAttr.name && $0.key != "privateAttrs" } - expect(AnyComparer.isEqual(userDictionaryWithoutRedacted, to: includingDictionaryWithoutRedacted)) == true - } else { - let userDictionaryWithoutRedacted = userDictionary.filter { $0.key != "custom" && $0.key != "privateAttrs" } - let includingDictionaryWithoutRedacted = includingDictionary.filter { $0.key != "custom" && $0.key != "privateAttrs" } - expect(AnyComparer.isEqual(userDictionaryWithoutRedacted, to: includingDictionaryWithoutRedacted)) == true - let expectedCustom = (includingDictionary["custom"] as! [String: Any]).filter { $0.key != privateAttr.name } - expect(AnyComparer.isEqual(userDictionary["custom"], to: expectedCustom)) == true - } - } - } - } - } - - context("with allUserAttributesPrivate") { - beforeEach { - config.allUserAttributesPrivate = true - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - it("creates expected dictionary") { - expect(userDictionary.count) == 3 - // Required attributes - expect(userDictionary[LDUser.CodingKeys.key.rawValue] as? String) == user.key - expect(userDictionary[LDUser.CodingKeys.isAnonymous.rawValue] as? Bool) == user.isAnonymous - - expect(Set(userDictionary.redactedAttributes!)) == Set(optionalNames + allCustomPrivitizable) - } - } - - context("with no private attributes") { - let noPrivateAssertions = { - it("matches dictionary including private") { - expect(AnyComparer.isEqual(userDictionary, to: user.dictionaryValue(includePrivateAttributes: true, config: config))) == true - } - } - context("by setting private attributes to nil") { - beforeEach { - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - noPrivateAssertions() - } - context("by setting config private attributes to empty") { - beforeEach { - config.privateUserAttributes = [] - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - noPrivateAssertions() - } - context("by setting user private attributes to empty") { - beforeEach { - user.privateAttributes = [] - userDictionary = user.dictionaryValue(includePrivateAttributes: false, config: config) - } - noPrivateAssertions() - } - } - } - } - - private func isEqualSpec() { - var user: LDUser! - var otherUser: LDUser! - - describe("isEqual") { - context("when users are equal") { - it("returns true with all properties set") { - user = LDUser.stub() - otherUser = user - expect(user.isEqual(to: otherUser)) == true - } - it("returns true with no properties set") { - user = LDUser() - otherUser = user - expect(user.isEqual(to: otherUser)) == true - } - } - context("when users are not equal") { - let testFields: [(String, Bool, LDValue, (inout LDUser, LDValue?) -> Void)] = - [("key", false, "dummy", { u, v in u.key = v!.stringValue() }), - ("secondary", true, "dummy", { u, v in u.secondary = v?.stringValue() }), - ("name", true, "dummy", { u, v in u.name = v?.stringValue() }), - ("firstName", true, "dummy", { u, v in u.firstName = v?.stringValue() }), - ("lastName", true, "dummy", { u, v in u.lastName = v?.stringValue() }), - ("country", true, "dummy", { u, v in u.country = v?.stringValue() }), - ("ipAddress", true, "dummy", { u, v in u.ipAddress = v?.stringValue() }), - ("email address", true, "dummy", { u, v in u.email = v?.stringValue() }), - ("avatar", true, "dummy", { u, v in u.avatar = v?.stringValue() }), - ("custom", false, ["dummy": true], { u, v in u.custom = (v!.toAny() as! [String: Any]).mapValues { LDValue.fromAny($0) } }), - ("isAnonymous", false, true, { u, v in u.isAnonymous = v!.booleanValue() }), - ("privateAttributes", false, "dummy", { u, v in u.privateAttributes = [UserAttribute.forName(v!.stringValue())] })] - testFields.forEach { name, isOptional, otherVal, setter in - context("\(name) differs") { - beforeEach { - user = LDUser.stub() - otherUser = user - } - context("and both exist") { - it("returns false") { - setter(&otherUser, otherVal) - expect(user.isEqual(to: otherUser)) == false - expect(otherUser.isEqual(to: user)) == false - } - } - if isOptional { - context("self \(name) nil") { - it("returns false") { - setter(&user, nil) - expect(user.isEqual(to: otherUser)) == false - } - } - context("other \(name) nil") { - it("returns false") { - setter(&otherUser, nil) - expect(user.isEqual(to: otherUser)) == false - } - } - } - } - } - } - } - } -} - -extension LDUser { - public func dictionaryValueWithAllAttributes() -> [String: Any] { - var dictionary = dictionaryValue(includePrivateAttributes: true, config: LDConfig.stub) - dictionary[CodingKeys.privateAttributes.rawValue] = privateAttributes - return dictionary - } -} - -extension Dictionary where Key == String, Value == Any { - fileprivate var redactedAttributes: [String]? { - self[LDUser.CodingKeys.privateAttributes.rawValue] as? [String] - } - fileprivate func customDictionary() -> [String: Any]? { - self[LDUser.CodingKeys.custom.rawValue] as? [String: Any] - } } diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index ac5a5b4d..292c2c97 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift @@ -110,8 +110,8 @@ final class DarklyServiceSpec: QuickSpec { expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasPrefix("/\(DarklyService.FlagRequestPath.get)")).to(beTrue()) - let expectedUser = testContext.user.dictionaryValue(includePrivateAttributes: true, config: testContext.config) - expect(AnyComparer.isEqual(urlRequest?.url?.lastPathComponent.jsonDictionary, to: expectedUser)) == true + let expectedUser = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) + expect(urlRequest?.url?.lastPathComponent.jsonValue) == expectedUser } else { fail("request path is missing") } @@ -163,8 +163,8 @@ final class DarklyServiceSpec: QuickSpec { expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasPrefix("/\(DarklyService.FlagRequestPath.get)")).to(beTrue()) - let expectedUser = testContext.user.dictionaryValue(includePrivateAttributes: true, config: testContext.config) - expect(AnyComparer.isEqual(urlRequest?.url?.lastPathComponent.jsonDictionary, to: expectedUser)) == true + let expectedUser = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) + expect(urlRequest?.url?.lastPathComponent.jsonValue) == expectedUser } else { fail("request path is missing") } @@ -538,8 +538,8 @@ final class DarklyServiceSpec: QuickSpec { let receivedArguments = testContext.serviceFactoryMock.makeStreamingProviderReceivedArguments expect(receivedArguments!.url.host) == testContext.config.streamUrl.host expect(receivedArguments!.url.pathComponents.contains(DarklyService.StreamRequestPath.meval)).to(beTrue()) - let expectedUser = testContext.user.dictionaryValue(includePrivateAttributes: true, config: testContext.config) - expect(AnyComparer.isEqual(receivedArguments!.url.lastPathComponent.jsonDictionary, to: expectedUser)) == true + let expectedUser = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) + expect(receivedArguments!.url.lastPathComponent.jsonValue) == expectedUser expect(receivedArguments!.httpHeaders).toNot(beEmpty()) expect(receivedArguments!.connectMethod).to(be("GET")) expect(receivedArguments!.connectBody).to(beNil()) @@ -559,8 +559,8 @@ final class DarklyServiceSpec: QuickSpec { expect(receivedArguments!.url.lastPathComponent) == DarklyService.StreamRequestPath.meval expect(receivedArguments!.httpHeaders).toNot(beEmpty()) expect(receivedArguments!.connectMethod) == DarklyService.HTTPRequestMethod.report - let expectedUser = testContext.user.dictionaryValue(includePrivateAttributes: true, config: testContext.config) - expect(AnyComparer.isEqual(receivedArguments!.connectBody?.jsonDictionary, to: expectedUser)) == true + let expectedUser = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) + expect(try? JSONDecoder().decode(LDValue.self, from: receivedArguments!.connectBody!)) == expectedUser } } } @@ -765,8 +765,10 @@ private extension Data { } private extension String { - var jsonDictionary: [String: Any]? { + var jsonValue: LDValue? { let base64encodedString = self.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") - return Data(base64Encoded: base64encodedString)?.jsonDictionary + guard let data = Data(base64Encoded: base64encodedString) + else { return nil } + return try? JSONDecoder().decode(LDValue.self, from: data) } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift index 0b56d4fb..9a75b969 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DeprecatedCacheModelV5Spec.swift @@ -52,10 +52,10 @@ final class DeprecatedCacheModelV5Spec: QuickSpec, CacheModelTestInterface { extension LDUser { func modelV5DictionaryValue(including featureFlags: [LDFlagKey: FeatureFlag], using lastUpdated: Date?) -> [String: Any] { - var userDictionary = dictionaryValueWithAllAttributes() - userDictionary.setLastUpdated(lastUpdated) + var userDictionary = encodeToLDValue(self, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true])?.toAny() as! [String: Any] + userDictionary[CodingKeys.privateAttributes.rawValue] = privateAttributes + userDictionary["updatedAt"] = lastUpdated?.stringValue userDictionary[LDUser.CodingKeys.config.rawValue] = featureFlags.compactMapValues { $0.modelV5dictionaryValue } - return userDictionary } }