From b63014754e47d6995150b1ebc5c7090cfba00add Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Wed, 7 Dec 2022 10:33:50 -0600 Subject: [PATCH] prepare 8.0.0 release (#286) ## [8.0.0] - 2022-12-07 The latest version of this SDK supports LaunchDarkly's new custom contexts feature. Contexts are an evolution of a previously-existing concept, "users." Contexts let you create targeting rules for feature flags based on a variety of different information, including attributes pertaining to users, organizations, devices, and more. You can even combine contexts to create "multi-contexts." This feature is only available to members of LaunchDarkly's Early Access Program (EAP). If you're in the EAP, you can use contexts by updating your SDK to the latest version and, if applicable, updating your Relay Proxy. Outdated SDK versions do not support contexts, and will cause unpredictable flag evaluation behavior. If you are not in the EAP, only use single contexts of kind "user", or continue to use the user type if available. If you try to create contexts, the context will be sent to LaunchDarkly, but any data not related to the user object will be ignored. For detailed information about this version, please refer to the list below. For information on how to upgrade from the previous version, please read the migration guide for [Swift](https://docs.launchdarkly.com/sdk/client-side/ios/migration-7-to-8-swift) or [Objective-C](https://docs.launchdarkly.com/sdk/client-side/ios/migration-7-to-8-objc). ### Added: - The type `LDContext` defines the new context model. - For all SDK methods that took an `LDUser` parameter, there is now an overload that takes an `LDContext`. ### Changed: - The `secondary` attribute which existed in `LDUser` is no longer a supported feature. If you set an attribute with that name in `LDContext`, it will simply be a custom attribute like any other. - Analytics event data now uses a new JSON schema due to differences between the context model and the old user model. - The SDK no longer adds `device` and `os` values to the user attributes. Applications that wish to use device/OS information in feature flag rules must explicitly add such information. ### Removed: - Removed the `secondary` meta-attribute in `LDUser`. - The `alias` method no longer exists because alias events are not needed in the new context model. - The `autoAliasingOptOut` and `inlineUsersInEvents` options no longer exist because they are not relevant in the new context model. --- .jazzy.yaml | 9 +- .swiftlint.yml | 9 +- CHANGELOG.md | 23 + ContractTests/.swiftlint.yml | 19 +- ContractTests/Package.swift | 2 +- .../Source/Controllers/SdkController.swift | 112 ++- ContractTests/Source/Models/client.swift | 7 +- ContractTests/Source/Models/command.swift | 59 +- ContractTests/Source/Models/user.swift | 7 +- ContractTests/Source/main.swift | 4 +- ContractTests/testharness-suppressions.txt | 54 -- LaunchDarkly.podspec | 2 +- LaunchDarkly.xcodeproj/project.pbxproj | 242 +++-- .../contents.xcworkspacedata | 3 + .../xcschemes/ContractTests.xcscheme | 87 ++ .../GeneratedCode/mocks.generated.swift | 35 +- LaunchDarkly/LaunchDarkly/LDClient.swift | 170 ++-- .../LaunchDarkly/LDClientVariation.swift | 12 +- LaunchDarkly/LaunchDarkly/LDCommon.swift | 2 +- .../Models/ConnectionInformation.swift | 2 +- .../LaunchDarkly/Models/Context/Kind.swift | 91 ++ .../Models/Context/LDContext.swift | 909 ++++++++++++++++++ .../Models/Context/Reference.swift | 232 +++++ .../LaunchDarkly/Models/DiagnosticEvent.swift | 10 +- LaunchDarkly/LaunchDarkly/Models/Event.swift | 78 +- .../FeatureFlag/FlagRequestTracker.swift | 14 +- .../FeatureFlag/LDEvaluationDetail.swift | 2 +- .../LaunchDarkly/Models/LDConfig.swift | 72 +- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 98 +- .../LaunchDarkly/Models/UserAttribute.swift | 4 +- .../Networking/DarklyService.swift | 32 +- .../LaunchDarkly/Networking/HTTPHeaders.swift | 4 +- .../ObjectiveC/ObjcLDClient.swift | 131 +-- .../ObjectiveC/ObjcLDConfig.swift | 44 +- .../ObjectiveC/ObjcLDContext.swift | 304 ++++++ .../ObjectiveC/ObjcLDEvaluationDetail.swift | 18 +- .../ObjectiveC/ObjcLDReference.swift | 38 + .../LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 24 +- .../ServiceObjects/Cache/CacheConverter.swift | 23 +- .../Cache/FeatureFlagCache.swift | 41 +- .../ServiceObjects/ClientServiceFactory.swift | 22 +- .../ServiceObjects/EnvironmentReporter.swift | 23 +- .../ServiceObjects/EventReporter.swift | 33 +- .../ServiceObjects/FlagSynchronizer.swift | 10 +- LaunchDarkly/LaunchDarkly/Util.swift | 13 + .../LaunchDarklyTests/LDClientSpec.swift | 235 ++--- .../Mocks/ClientServiceMockFactory.swift | 24 +- .../Mocks/DarklyServiceMock.swift | 14 +- .../Mocks/EnvironmentReportingMock.swift | 1 - .../Mocks/LDContextStub.swift | 50 + .../LaunchDarklyTests/Mocks/LDUserStub.swift | 4 +- .../Models/Context/KindSpec.swift | 72 ++ .../Models/Context/LDContextCodableSpec.swift | 146 +++ .../Models/Context/LDContextSpec.swift | 290 ++++++ .../Models/Context/ReferenceSpec.swift | 83 ++ .../Models/DiagnosticEventSpec.swift | 20 +- .../LaunchDarklyTests/Models/EventSpec.swift | 159 ++- .../FlagRequestTracking/FlagCounterSpec.swift | 44 +- .../FlagRequestTrackerSpec.swift | 22 +- .../Models/LDConfigSpec.swift | 35 +- .../Models/User/LDUserSpec.swift | 18 +- .../Models/User/LDUserToContextSpec.swift | 64 ++ .../Networking/DarklyServiceSpec.swift | 40 +- .../Networking/HTTPHeadersSpec.swift | 2 +- .../Cache/CacheConverterSpec.swift | 4 +- .../Cache/FeatureFlagCacheSpec.swift | 72 +- .../ServiceObjects/EventReporterSpec.swift | 52 +- .../ServiceObjects/FlagStoreSpec.swift | 2 +- Makefile | 2 +- README.md | 6 +- 70 files changed, 3523 insertions(+), 1068 deletions(-) create mode 100644 LaunchDarkly.xcworkspace/xcshareddata/xcschemes/ContractTests.xcscheme create mode 100644 LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift create mode 100644 LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift create mode 100644 LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift create mode 100644 LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift create mode 100644 LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDReference.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/Models/Context/KindSpec.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/Models/Context/ReferenceSpec.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/Models/User/LDUserToContextSpec.swift diff --git a/.jazzy.yaml b/.jazzy.yaml index d9d3f5b1..e055ff36 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -17,7 +17,11 @@ custom_categories: children: - LDClient - LDConfig + - LDContext + - LDContextBuilder - LDUser + - Reference + - LDMultiContextBuilder - LDEvaluationDetail - LDValue @@ -36,7 +40,6 @@ custom_categories: - name: Other Types children: - - UserAttribute - LDStreamingMode - LDFlagKey - LDInvalidArgumentError @@ -46,8 +49,10 @@ custom_categories: children: - ObjcLDClient - ObjcLDConfig - - ObjcLDUser + - ObjcLDReference + - ObjcLDContext - ObjcLDChangedFlag + - ObjcLDUser - ObjcLDValue - ObjcLDValueType diff --git a/.swiftlint.yml b/.swiftlint.yml index 71ad606f..e30f6a06 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -2,7 +2,6 @@ disabled_rules: - line_length - - trailing_whitespace opt_in_rules: - contains_over_filter_count @@ -57,4 +56,12 @@ identifier_name: - lhs - rhs +trailing_whitespace: + severity: error + +missing_docs: + error: + - open + - public + reporter: "xcode" diff --git a/CHANGELOG.md b/CHANGELOG.md index 252dbfc8..65d3d019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ All notable changes to the LaunchDarkly iOS SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [8.0.0] - 2022-12-07 +The latest version of this SDK supports LaunchDarkly's new custom contexts feature. Contexts are an evolution of a previously-existing concept, "users." Contexts let you create targeting rules for feature flags based on a variety of different information, including attributes pertaining to users, organizations, devices, and more. You can even combine contexts to create "multi-contexts." + +This feature is only available to members of LaunchDarkly's Early Access Program (EAP). If you're in the EAP, you can use contexts by updating your SDK to the latest version and, if applicable, updating your Relay Proxy. Outdated SDK versions do not support contexts, and will cause unpredictable flag evaluation behavior. + +If you are not in the EAP, only use single contexts of kind "user", or continue to use the user type if available. If you try to create contexts, the context will be sent to LaunchDarkly, but any data not related to the user object will be ignored. + +For detailed information about this version, please refer to the list below. For information on how to upgrade from the previous version, please read the migration guide for [Swift](https://docs.launchdarkly.com/sdk/client-side/ios/migration-7-to-8-swift) or [Objective-C](https://docs.launchdarkly.com/sdk/client-side/ios/migration-7-to-8-objc). + +### Added: +- The type `LDContext` defines the new context model. +- For all SDK methods that took an `LDUser` parameter, there is now an overload that takes an `LDContext`. + +### Changed: +- The `secondary` attribute which existed in `LDUser` is no longer a supported feature. If you set an attribute with that name in `LDContext`, it will simply be a custom attribute like any other. +- Analytics event data now uses a new JSON schema due to differences between the context model and the old user model. +- The SDK no longer adds `device` and `os` values to the user attributes. Applications that wish to use device/OS information in feature flag rules must explicitly add such information. + +### Removed: +- Removed the `secondary` meta-attribute in `LDUser`. +- The `alias` method no longer exists because alias events are not needed in the new context model. +- The `autoAliasingOptOut` and `inlineUsersInEvents` options no longer exist because they are not relevant in the new context model. + ## [7.1.0] - 2022-11-08 ### Added: - Added Objective C bindings for ApplicationInfo. diff --git a/ContractTests/.swiftlint.yml b/ContractTests/.swiftlint.yml index 61290d2c..7ee54985 100644 --- a/ContractTests/.swiftlint.yml +++ b/ContractTests/.swiftlint.yml @@ -1,6 +1,9 @@ +# See test subconfiguration at `LaunchDarkly/LaunchDarklyTests/.swiftlint.yml` + disabled_rules: + - cyclomatic_complexity - line_length - - trailing_whitespace + - todo opt_in_rules: - contains_over_filter_count @@ -26,8 +29,8 @@ included: excluded: function_body_length: - warning: 50 - error: 70 + warning: 70 + error: 90 type_body_length: warning: 300 @@ -39,7 +42,7 @@ file_length: identifier_name: min_length: # only min_length - warning: 3 # only warning + warning: 2 # only warning max_length: warning: 50 error: 60 @@ -54,4 +57,12 @@ identifier_name: - lhs - rhs +trailing_whitespace: + severity: error + +missing_docs: + error: + - open + - public + reporter: "xcode" diff --git a/ContractTests/Package.swift b/ContractTests/Package.swift index 658a3500..3341987b 100644 --- a/ContractTests/Package.swift +++ b/ContractTests/Package.swift @@ -26,6 +26,6 @@ let package = Package( .product(name: "LaunchDarkly", package: "LaunchDarkly"), .product(name: "Vapor", package: "vapor") ], - path: "Source"), + path: "Source") ], swiftLanguageVersions: [.v5]) diff --git a/ContractTests/Source/Controllers/SdkController.swift b/ContractTests/Source/Controllers/SdkController.swift index 132a6d73..f3d5e394 100644 --- a/ContractTests/Source/Controllers/SdkController.swift +++ b/ContractTests/Source/Controllers/SdkController.swift @@ -2,7 +2,7 @@ import Vapor import LaunchDarkly final class SdkController: RouteCollection { - private var clients: [Int : LDClient] = [:] + private var clients: [Int: LDClient] = [:] private var clientCounter = 0 func boot(routes: RoutesBuilder) { @@ -20,7 +20,9 @@ final class SdkController: RouteCollection { "client-side", "mobile", "service-endpoints", - "tags" + "strongly-typed", + "tags", + "user-type" ] return StatusResponse( @@ -61,20 +63,16 @@ final class SdkController: RouteCollection { } if let allPrivate = events.allAttributesPrivate { - config.allUserAttributesPrivate = allPrivate + config.allContextAttributesPrivate = allPrivate } if let globalPrivate = events.globalPrivateAttributes { - config.privateUserAttributes = globalPrivate.map({ UserAttribute.forName($0) }) + config.privateContextAttributes = globalPrivate.map { Reference($0) } } if let flushIntervalMs = events.flushIntervalMs { config.eventFlushInterval = flushIntervalMs } - - if let inlineUsers = events.inlineUsers { - config.inlineUserInEvents = inlineUsers - } } if let tags = createInstance.configuration.tags { @@ -92,10 +90,6 @@ final class SdkController: RouteCollection { let clientSide = createInstance.configuration.clientSide - if let autoAliasingOptOut = clientSide.autoAliasingOptOut { - config.autoAliasingOptOut = autoAliasingOptOut - } - if let evaluationReasons = clientSide.evaluationReasons { config.evaluationReasons = evaluationReasons } @@ -107,8 +101,14 @@ final class SdkController: RouteCollection { let dispatchSemaphore = DispatchSemaphore(value: 0) let startWaitSeconds = (createInstance.configuration.startWaitTimeMs ?? 5_000) / 1_000 - LDClient.start(config:config, user: clientSide.initialUser, startWaitSeconds: startWaitSeconds) { timedOut in - dispatchSemaphore.signal() + if let context = clientSide.initialContext { + LDClient.start(config: config, context: context, startWaitSeconds: startWaitSeconds) { _ in + dispatchSemaphore.signal() + } + } else if let user = clientSide.initialUser { + LDClient.start(config: config, user: user, startWaitSeconds: startWaitSeconds) { _ in + dispatchSemaphore.signal() + } } dispatchSemaphore.wait() @@ -159,17 +159,67 @@ final class SdkController: RouteCollection { return CommandResponse.evaluateAll(result) case "identifyEvent": let semaphore = DispatchSemaphore(value: 0) - client.identify(user: commandParameters.identifyEvent!.user) { - semaphore.signal() + if let context = commandParameters.identifyEvent!.context { + client.identify(context: context) { + semaphore.signal() + } + } else if let user = commandParameters.identifyEvent!.user { + client.identify(user: user) { + semaphore.signal() + } } semaphore.wait() - case "aliasEvent": - client.alias(context: commandParameters.aliasEvent!.user, previousContext: commandParameters.aliasEvent!.previousUser) case "customEvent": let event = commandParameters.customEvent! client.track(key: event.eventKey, data: event.data, metricValue: event.metricValue) case "flushEvents": client.flush() + case "contextBuild": + let contextBuild = commandParameters.contextBuild! + + do { + if let singleParams = contextBuild.single { + let context = try SdkController.buildSingleContextFromParams(singleParams) + + let encoder = JSONEncoder() + let output = try encoder.encode(context) + + let response = ContextBuildResponse(output: String(data: Data(output), encoding: .utf8)) + return CommandResponse.contextBuild(response) + } + + if let multiParams = contextBuild.multi { + var multiContextBuilder = LDMultiContextBuilder() + try multiParams.forEach { + multiContextBuilder.addContext(try SdkController.buildSingleContextFromParams($0)) + } + + let context = try multiContextBuilder.build().get() + let encoder = JSONEncoder() + let output = try encoder.encode(context) + + let response = ContextBuildResponse(output: String(data: Data(output), encoding: .utf8)) + return CommandResponse.contextBuild(response) + } + } catch { + let response = ContextBuildResponse(output: nil, error: error.localizedDescription) + return CommandResponse.contextBuild(response) + } + case "contextConvert": + let convertRequest = commandParameters.contextConvert! + do { + let decoder = JSONDecoder() + let context: LDContext = try decoder.decode(LDContext.self, from: Data(convertRequest.input.utf8)) + + let encoder = JSONEncoder() + let output = try encoder.encode(context) + + let response = ContextBuildResponse(output: String(data: Data(output), encoding: .utf8)) + return CommandResponse.contextBuild(response) + } catch { + let response = ContextBuildResponse(output: nil, error: error.localizedDescription) + return CommandResponse.contextBuild(response) + } default: throw Abort(.badRequest) } @@ -177,6 +227,32 @@ final class SdkController: RouteCollection { return CommandResponse.ok } + static func buildSingleContextFromParams(_ params: SingleContextParameters) throws -> LDContext { + var contextBuilder = LDContextBuilder(key: params.key) + + if let kind = params.kind { + contextBuilder.kind(kind) + } + + if let name = params.name { + contextBuilder.name(name) + } + + if let anonymous = params.anonymous { + contextBuilder.anonymous(anonymous) + } + + if let privateAttributes = params.privateAttribute { + privateAttributes.forEach { contextBuilder.addPrivateAttribute(Reference($0)) } + } + + if let custom = params.custom { + custom.forEach { contextBuilder.trySetValue($0.key, $0.value) } + } + + return try contextBuilder.build().get() + } + func evaluate(_ client: LDClient, _ params: EvaluateFlagParameters) throws -> EvaluateFlagResponse { switch params.valueType { case "bool": diff --git a/ContractTests/Source/Models/client.swift b/ContractTests/Source/Models/client.swift index f7d42854..a072c4e7 100644 --- a/ContractTests/Source/Models/client.swift +++ b/ContractTests/Source/Models/client.swift @@ -10,6 +10,7 @@ struct Configuration: Content { var credential: String var startWaitTimeMs: Double? var initCanFail: Bool? + // TODO(mmk) Add serviceEndpoints var streaming: StreamingParameters? var polling: PollingParameters? var events: EventParameters? @@ -24,6 +25,7 @@ struct StreamingParameters: Content { struct PollingParameters: Content { var baseUri: String? + // TODO(mmk) Add pollIntervalMs } struct EventParameters: Content { @@ -33,7 +35,6 @@ struct EventParameters: Content { var allAttributesPrivate: Bool? var globalPrivateAttributes: [String]? var flushIntervalMs: Double? - var inlineUsers: Bool? } struct TagParameters: Content { @@ -42,8 +43,8 @@ struct TagParameters: Content { } struct ClientSideParameters: Content { - var initialUser: LDUser - var autoAliasingOptOut: Bool? + var initialContext: LDContext? + var initialUser: LDUser? var evaluationReasons: Bool? var useReport: Bool? } diff --git a/ContractTests/Source/Models/command.swift b/ContractTests/Source/Models/command.swift index 415b2aaa..567f784c 100644 --- a/ContractTests/Source/Models/command.swift +++ b/ContractTests/Source/Models/command.swift @@ -4,24 +4,30 @@ import LaunchDarkly enum CommandResponse: Content, Encodable { case evaluateFlag(EvaluateFlagResponse) case evaluateAll(EvaluateAllFlagsResponse) + case contextBuild(ContextBuildResponse) + case contextConvert(ContextBuildResponse) case ok func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - if case let CommandResponse.evaluateFlag(response) = self { + switch self { + case .evaluateFlag(let response): try container.encode(response) return - } - - if case let CommandResponse.evaluateAll(response) = self { + case .evaluateAll(let response): try container.encode(response) return + case .contextBuild(let response): + try container.encode(response) + return + case .contextConvert(let response): + try container.encode(response) + return + case .ok: + try container.encode(true) + return } - - try container.encode(true) - - return } } @@ -31,7 +37,8 @@ struct CommandParameters: Content { var evaluateAll: EvaluateAllFlagsParameters? var customEvent: CustomEventParameters? var identifyEvent: IdentifyEventParameters? - var aliasEvent: AliasEventParameters? + var contextBuild: ContextBuildParameters? + var contextConvert: ContextConvertParameters? } struct EvaluateFlagParameters: Content { @@ -48,6 +55,7 @@ struct EvaluateFlagResponse: Content { } struct EvaluateAllFlagsParameters: Content { + // TODO(mmk) Add support for withReasons, clientSideOnly, and detailsOnlyForTrackedFlags } struct EvaluateAllFlagsResponse: Content { @@ -61,11 +69,34 @@ struct CustomEventParameters: Content { var metricValue: Double? } -struct IdentifyEventParameters: Content, Decodable { - var user: LDUser +struct IdentifyEventParameters: Content, Decodable { + var context: LDContext? + var user: LDUser? +} + +struct ContextBuildParameters: Content, Decodable { + var single: SingleContextParameters? + var multi: [SingleContextParameters]? +} + +struct SingleContextParameters: Content, Decodable { + var kind: String? + var key: String + var name: String? + var anonymous: Bool? + var privateAttribute: [String]? + var custom: [String: LDValue]? + + private enum CodingKeys: String, CodingKey { + case kind, key, name, anonymous, privateAttribute = "private", custom + } +} + +struct ContextBuildResponse: Content, Encodable { + var output: String? + var error: String? } -struct AliasEventParameters: Content { - var user: LDUser - var previousUser: LDUser +struct ContextConvertParameters: Content, Decodable { + var input: String } diff --git a/ContractTests/Source/Models/user.swift b/ContractTests/Source/Models/user.swift index 5bdf696c..51542c93 100644 --- a/ContractTests/Source/Models/user.swift +++ b/ContractTests/Source/Models/user.swift @@ -6,7 +6,7 @@ extension LDUser: Decodable { /// String keys associated with LDUser properties. public enum CodingKeys: String, CodingKey { /// Key names match the corresponding LDUser property - case key, name, firstName, lastName, country, ipAddress = "ip", email, avatar, custom, isAnonymous = "anonymous", device, operatingSystem = "os", config, privateAttributes = "privateAttributeNames", secondary + case key, name, firstName, lastName, country, ipAddress = "ip", email, avatar, custom, isAnonymous = "anonymous", device, operatingSystem = "os", config, privateAttributes = "privateAttributeNames" } public init(from decoder: Decoder) throws { @@ -23,8 +23,7 @@ extension LDUser: Decodable { avatar = try values.decodeIfPresent(String.self, forKey: .avatar) custom = try values.decodeIfPresent([String: LDValue].self, forKey: .custom) ?? [:] isAnonymous = try values.decodeIfPresent(Bool.self, forKey: .isAnonymous) ?? false - let _ = try values.decodeIfPresent([String].self, forKey: .privateAttributes) - privateAttributes = (try values.decodeIfPresent([String].self, forKey: .privateAttributes) ?? []).map({ UserAttribute.forName($0) }) - secondary = try values.decodeIfPresent(String.self, forKey: .secondary) + _ = try values.decodeIfPresent([String].self, forKey: .privateAttributes) + privateAttributes = (try values.decodeIfPresent([String].self, forKey: .privateAttributes) ?? []).map { UserAttribute.forName($0) } } } diff --git a/ContractTests/Source/main.swift b/ContractTests/Source/main.swift index 011d6f3a..a1538ddd 100644 --- a/ContractTests/Source/main.swift +++ b/ContractTests/Source/main.swift @@ -2,6 +2,7 @@ import Foundation import Vapor let semaphore = DispatchSemaphore(value: 0) + DispatchQueue.global(qos: .userInitiated).async { do { var env = try Environment.detect() @@ -16,6 +17,7 @@ DispatchQueue.global(qos: .userInitiated).async { } let runLoop = RunLoop.current -while (semaphore.wait(timeout: .now()) == .timedOut) { + +while semaphore.wait(timeout: .now()) == .timedOut { runLoop.run(mode: .default, before: .distantFuture) } diff --git a/ContractTests/testharness-suppressions.txt b/ContractTests/testharness-suppressions.txt index 87c48761..e69de29b 100644 --- a/ContractTests/testharness-suppressions.txt +++ b/ContractTests/testharness-suppressions.txt @@ -1,54 +0,0 @@ -events/requests/method and headers -evaluation/parameterized/evaluationReasons=false/basic values - bool/flag1-bool/evaluate flag with detail -evaluation/parameterized/evaluationReasons=false/basic values - bool/flag1-bool/evaluate all flags -evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate flag without detail -evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate flag with detail -evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate all flags -streaming/requests/user properties/GET -streaming/requests/user properties/REPORT -polling/requests/method and headers/GET -polling/requests/method and headers/REPORT -polling/requests/URL path is computed correctly/base URI has no trailing slash/GET -polling/requests/URL path is computed correctly/base URI has no trailing slash/REPORT -polling/requests/URL path is computed correctly/base URI has a trailing slash/GET -polling/requests/URL path is computed correctly/base URI has a trailing slash/REPORT -polling/requests/query parameters/evaluationReasons set to [none]/GET -polling/requests/query parameters/evaluationReasons set to [none]/REPORT -polling/requests/query parameters/evaluationReasons set to false/GET -polling/requests/query parameters/evaluationReasons set to false/REPORT -polling/requests/query parameters/evaluationReasons set to true/GET -polling/requests/query parameters/evaluationReasons set to true/REPORT -polling/requests/user properties/GET -polling/requests/user properties/REPORT -events/user properties/inlineUsers=false/user-private=none/identify event -events/user properties/inlineUsers=false/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=false, globally-private=[firstName]/user-private=none/identify event -events/user properties/inlineUsers=false, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=false, allAttributesPrivate=true/user-private=none/identify event -events/user properties/inlineUsers=false, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=false, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/identify event -events/user properties/inlineUsers=false, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=true/user-private=none/identify event -events/user properties/inlineUsers=true/user-private=none/feature event -events/user properties/inlineUsers=true/user-private=none/custom event -events/user properties/inlineUsers=true/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=true/user-private=[lastName preferredLanguage]/feature event -events/user properties/inlineUsers=true/user-private=[lastName preferredLanguage]/custom event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=none/identify event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=none/feature event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=none/custom event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/feature event -events/user properties/inlineUsers=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/custom event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=none/identify event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=none/feature event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=none/custom event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/feature event -events/user properties/inlineUsers=true, allAttributesPrivate=true/user-private=[lastName preferredLanguage]/custom event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/identify event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/feature event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=none/custom event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/identify event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/feature event -events/user properties/inlineUsers=true, allAttributesPrivate=true, globally-private=[firstName]/user-private=[lastName preferredLanguage]/custom event diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index fd01eece..36e9dcf4 100644 --- a/LaunchDarkly.podspec +++ b/LaunchDarkly.podspec @@ -2,7 +2,7 @@ Pod::Spec.new do |ld| ld.name = "LaunchDarkly" - ld.version = "7.1.0" + ld.version = "8.0.0" ld.summary = "iOS SDK for LaunchDarkly" ld.description = <<-DESC diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 4edf5b62..23190891 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -7,10 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 29A4C47527DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; - 29A4C47627DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; - 29A4C47727DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; - 29A4C47827DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; }; 29F9D19E2812E005008D12C0 /* ObjcLDValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */; }; 29F9D19F2812E005008D12C0 /* ObjcLDValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */; }; 29F9D1A02812E005008D12C0 /* ObjcLDValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */; }; @@ -28,7 +24,6 @@ 831188432113ADBE00D77CB5 /* LDCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B6C4B51F4DE7630055351C /* LDCommon.swift */; }; 831188442113ADC200D77CB5 /* LDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDD1F26380700C05156 /* LDConfig.swift */; }; 831188452113ADC500D77CB5 /* LDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDC1F26380700C05156 /* LDClient.swift */; }; - 831188462113ADCA00D77CB5 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */; }; 8311884A2113ADD700D77CB5 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFE41F263DAC00C05156 /* FeatureFlag.swift */; }; 8311884B2113ADDA00D77CB5 /* LDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F25D1F474E5900ECE1AF /* LDChangedFlag.swift */; }; 8311884C2113ADDE00D77CB5 /* FlagChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */; }; @@ -52,7 +47,6 @@ 831188672113AE4D00D77CB5 /* Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D2AAE2061AAA000B4AC3C /* Thread.swift */; }; 831188682113AE5600D77CB5 /* ObjcLDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */; }; 831188692113AE5900D77CB5 /* ObjcLDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */; }; - 8311886A2113AE5D00D77CB5 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */; }; 8311886C2113AE6400D77CB5 /* ObjcLDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */; }; 831188702113C2D300D77CB5 /* LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 8354EFC51F22491C00C05156 /* LaunchDarkly.h */; settings = {ATTRIBUTES = (Public, ); }; }; 831188712113C50A00D77CB5 /* LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 8354EFC51F22491C00C05156 /* LaunchDarkly.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -72,7 +66,6 @@ 831EF34320655E730001C643 /* LDCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B6C4B51F4DE7630055351C /* LDCommon.swift */; }; 831EF34420655E730001C643 /* LDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDD1F26380700C05156 /* LDConfig.swift */; }; 831EF34520655E730001C643 /* LDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDC1F26380700C05156 /* LDClient.swift */; }; - 831EF34620655E730001C643 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */; }; 831EF34A20655E730001C643 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFE41F263DAC00C05156 /* FeatureFlag.swift */; }; 831EF34B20655E730001C643 /* LDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F25D1F474E5900ECE1AF /* LDChangedFlag.swift */; }; 831EF34C20655E730001C643 /* FlagChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */; }; @@ -94,7 +87,6 @@ 831EF36520655E730001C643 /* Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D2AAE2061AAA000B4AC3C /* Thread.swift */; }; 831EF36620655E730001C643 /* ObjcLDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */; }; 831EF36720655E730001C643 /* ObjcLDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */; }; - 831EF36820655E730001C643 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */; }; 831EF36A20655E730001C643 /* ObjcLDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */; }; 832307A61F7D8D720029815A /* URLRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832307A51F7D8D720029815A /* URLRequestSpec.swift */; }; 832307A81F7DA61B0029815A /* LDEventSourceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832307A71F7DA61B0029815A /* LDEventSourceMock.swift */; }; @@ -127,7 +119,6 @@ 8358F2621F47747F00ECE1AF /* FlagChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */; }; 835E1D3F1F63450A00184DB4 /* ObjcLDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */; }; 835E1D401F63450A00184DB4 /* ObjcLDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */; }; - 835E1D411F63450A00184DB4 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */; }; 835E1D431F685AC900184DB4 /* ObjcLDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */; }; 835E4C54206BDF8D004C6E6C /* EnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831425B0206B030100F2EF36 /* EnvironmentReporter.swift */; }; 835E4C57206BF7E3004C6E6C /* LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 8354EFC51F22491C00C05156 /* LaunchDarkly.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -145,7 +136,6 @@ 83906A7B21190B7700D7D3C5 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8372668B20D4439600BD1088 /* DateFormatter.swift */; }; 8392FFA32033565700320914 /* HTTPURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8392FFA22033565700320914 /* HTTPURLResponse.swift */; }; 83A0E6B1203B557F00224298 /* FeatureFlagSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A0E6B0203B557F00224298 /* FeatureFlagSpec.swift */; }; - 83A2D6241F51CD7A00EA3BD4 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */; }; 83B1D7C92073F354006D1B1C /* CwlSysctl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B1D7C82073F354006D1B1C /* CwlSysctl.swift */; }; 83B6C4B61F4DE7630055351C /* LDCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B6C4B51F4DE7630055351C /* LDCommon.swift */; }; 83B6E3F1222EFA3800FF2A6A /* ThreadSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B6E3F0222EFA3800FF2A6A /* ThreadSpec.swift */; }; @@ -159,7 +149,6 @@ 83D9EC752062DEAB004D7FA6 /* LDCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B6C4B51F4DE7630055351C /* LDCommon.swift */; }; 83D9EC762062DEAB004D7FA6 /* LDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDD1F26380700C05156 /* LDConfig.swift */; }; 83D9EC772062DEAB004D7FA6 /* LDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFDC1F26380700C05156 /* LDClient.swift */; }; - 83D9EC782062DEAB004D7FA6 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */; }; 83D9EC7C2062DEAB004D7FA6 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354EFE41F263DAC00C05156 /* FeatureFlag.swift */; }; 83D9EC7D2062DEAB004D7FA6 /* LDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F25D1F474E5900ECE1AF /* LDChangedFlag.swift */; }; 83D9EC7E2062DEAB004D7FA6 /* FlagChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */; }; @@ -181,26 +170,63 @@ 83D9EC972062DEAB004D7FA6 /* Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D2AAE2061AAA000B4AC3C /* Thread.swift */; }; 83D9EC982062DEAB004D7FA6 /* ObjcLDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */; }; 83D9EC992062DEAB004D7FA6 /* ObjcLDConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */; }; - 83D9EC9A2062DEAB004D7FA6 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */; }; 83D9EC9C2062DEAB004D7FA6 /* ObjcLDChangedFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */; }; 83DDBEF61FA24A7E00E428B6 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEF51FA24A7E00E428B6 /* Data.swift */; }; 83DDBEFE1FA24F9600E428B6 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFD1FA24F9600E428B6 /* Date.swift */; }; 83DDBF001FA2589900E428B6 /* FlagStoreSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDBEFF1FA2589900E428B6 /* FlagStoreSpec.swift */; }; - 83E2E2061F9E7AC7007514E9 /* LDUserSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83E2E2051F9E7AC7007514E9 /* LDUserSpec.swift */; }; 83EBCBB120D9C7B5003A7142 /* FlagCounterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBCBB020D9C7B5003A7142 /* FlagCounterSpec.swift */; }; 83EBCBB320DABE1B003A7142 /* FlagRequestTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBCBB220DABE1B003A7142 /* FlagRequestTracker.swift */; }; 83EBCBB420DABE1B003A7142 /* FlagRequestTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBCBB220DABE1B003A7142 /* FlagRequestTracker.swift */; }; 83EBCBB520DABE1B003A7142 /* FlagRequestTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBCBB220DABE1B003A7142 /* FlagRequestTracker.swift */; }; 83EBCBB720DABE93003A7142 /* FlagRequestTrackerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBCBB620DABE93003A7142 /* FlagRequestTrackerSpec.swift */; }; 83EF67931F9945E800403126 /* EventSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67921F9945E800403126 /* EventSpec.swift */; }; - 83EF67951F994BAD00403126 /* LDUserStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67941F994BAD00403126 /* LDUserStub.swift */; }; 83F0A5641FB5F33800550A95 /* LDConfigSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F0A5631FB5F33800550A95 /* LDConfigSpec.swift */; }; 83FEF8DD1F266742001CF12C /* FlagSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83FEF8DC1F266742001CF12C /* FlagSynchronizer.swift */; }; 83FEF8DF1F2667E4001CF12C /* EventReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83FEF8DE1F2667E4001CF12C /* EventReporter.swift */; }; - A322B4CD28BE4D9E00A212ED /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A322B4CC28BE4D9E00A212ED /* LDSwiftEventSource */; }; - A322B4CF28BE4DA800A212ED /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A322B4CE28BE4DA800A212ED /* LDSwiftEventSource */; }; - A322B4D128BE4DB200A212ED /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A322B4D028BE4DB200A212ED /* LDSwiftEventSource */; }; - A322B4D328BE4DBA00A212ED /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A322B4D228BE4DBA00A212ED /* LDSwiftEventSource */; }; + A30EF4ED28C24A9A00CD220E /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A30EF4EC28C24A9A00CD220E /* LDSwiftEventSource */; }; + A30EF4EF28C24AA400CD220E /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A30EF4EE28C24AA400CD220E /* LDSwiftEventSource */; }; + A30EF4F128C24AAD00CD220E /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A30EF4F028C24AAD00CD220E /* LDSwiftEventSource */; }; + A30EF4F328C24AB600CD220E /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A30EF4F228C24AB600CD220E /* LDSwiftEventSource */; }; + A31088172837DC0400184942 /* Reference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088142837DC0400184942 /* Reference.swift */; }; + A31088182837DC0400184942 /* Reference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088142837DC0400184942 /* Reference.swift */; }; + A31088192837DC0400184942 /* Reference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088142837DC0400184942 /* Reference.swift */; }; + A310881A2837DC0400184942 /* Reference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088142837DC0400184942 /* Reference.swift */; }; + A310881B2837DC0400184942 /* Kind.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088152837DC0400184942 /* Kind.swift */; }; + A310881C2837DC0400184942 /* Kind.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088152837DC0400184942 /* Kind.swift */; }; + A310881D2837DC0400184942 /* Kind.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088152837DC0400184942 /* Kind.swift */; }; + A310881E2837DC0400184942 /* Kind.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088152837DC0400184942 /* Kind.swift */; }; + A310881F2837DC0400184942 /* LDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088162837DC0400184942 /* LDContext.swift */; }; + A31088202837DC0400184942 /* LDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088162837DC0400184942 /* LDContext.swift */; }; + A31088212837DC0400184942 /* LDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088162837DC0400184942 /* LDContext.swift */; }; + A31088222837DC0400184942 /* LDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088162837DC0400184942 /* LDContext.swift */; }; + A31088272837DCA900184942 /* LDContextSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088242837DCA900184942 /* LDContextSpec.swift */; }; + A31088282837DCA900184942 /* ReferenceSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088252837DCA900184942 /* ReferenceSpec.swift */; }; + A31088292837DCA900184942 /* KindSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088262837DCA900184942 /* KindSpec.swift */; }; + A33A5F7A28466D04000C29C7 /* LDContextStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A33A5F7928466D04000C29C7 /* LDContextStub.swift */; }; + A349D0332926CA0600DD5DE9 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0312926CA0600DD5DE9 /* UserAttribute.swift */; }; + A349D0342926CA0600DD5DE9 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0312926CA0600DD5DE9 /* UserAttribute.swift */; }; + A349D0352926CA0600DD5DE9 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0312926CA0600DD5DE9 /* UserAttribute.swift */; }; + A349D0362926CA0600DD5DE9 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0312926CA0600DD5DE9 /* UserAttribute.swift */; }; + A349D0372926CA0600DD5DE9 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0322926CA0600DD5DE9 /* LDUser.swift */; }; + A349D0382926CA0600DD5DE9 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0322926CA0600DD5DE9 /* LDUser.swift */; }; + A349D0392926CA0600DD5DE9 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0322926CA0600DD5DE9 /* LDUser.swift */; }; + A349D03A2926CA0600DD5DE9 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0322926CA0600DD5DE9 /* LDUser.swift */; }; + A349D03C2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */; }; + A349D03D2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */; }; + A349D03E2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */; }; + A349D03F2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */; }; + A355845529281CD70023D8EE /* LDUserSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D06B2926CB2600DD5DE9 /* LDUserSpec.swift */; }; + A355845729281CF00023D8EE /* LDUserStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A355845629281CF00023D8EE /* LDUserStub.swift */; }; + A355845929281E610023D8EE /* LDUserToContextSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A355845829281E610023D8EE /* LDUserToContextSpec.swift */; }; + A3570F5A28527B8200CF241A /* LDContextCodableSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3570F5928527B8200CF241A /* LDContextCodableSpec.swift */; }; + A36EDFC82853883400D91B05 /* ObjcLDReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFC72853883400D91B05 /* ObjcLDReference.swift */; }; + A36EDFC92853883400D91B05 /* ObjcLDReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFC72853883400D91B05 /* ObjcLDReference.swift */; }; + A36EDFCA2853883400D91B05 /* ObjcLDReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFC72853883400D91B05 /* ObjcLDReference.swift */; }; + A36EDFCB2853883400D91B05 /* ObjcLDReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFC72853883400D91B05 /* ObjcLDReference.swift */; }; + A36EDFCD2853C50B00D91B05 /* ObjcLDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */; }; + A36EDFCE2853C50B00D91B05 /* ObjcLDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */; }; + A36EDFCF2853C50B00D91B05 /* ObjcLDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */; }; + A36EDFD02853C50B00D91B05 /* ObjcLDContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */; }; A3799D4529033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3799D4429033665008D4A8E /* ObjcLDApplicationInfo.swift */; }; A3799D4629033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3799D4429033665008D4A8E /* ObjcLDApplicationInfo.swift */; }; A3799D4729033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3799D4429033665008D4A8E /* ObjcLDApplicationInfo.swift */; }; @@ -318,7 +344,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 29A4C47427DA6266005B8D34 /* UserAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAttribute.swift; sourceTree = ""; }; 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcLDValue.swift; sourceTree = ""; }; 29FE1297280413D4008CC918 /* Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = ""; }; 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPURLRequest.swift; sourceTree = ""; }; @@ -361,7 +386,6 @@ 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlagChangeObserver.swift; sourceTree = ""; }; 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDClient.swift; sourceTree = ""; }; 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDConfig.swift; sourceTree = ""; }; - 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDUser.swift; sourceTree = ""; }; 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDChangedFlag.swift; sourceTree = ""; }; 8372668B20D4439600BD1088 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = ""; }; 837406D321F760640087B22B /* LDTimerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDTimerSpec.swift; sourceTree = ""; }; @@ -373,7 +397,6 @@ 83906A762118EB9000D7D3C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8392FFA22033565700320914 /* HTTPURLResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPURLResponse.swift; sourceTree = ""; }; 83A0E6B0203B557F00224298 /* FeatureFlagSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagSpec.swift; sourceTree = ""; }; - 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDUser.swift; sourceTree = ""; }; 83B1D7C82073F354006D1B1C /* CwlSysctl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CwlSysctl.swift; sourceTree = ""; }; 83B6C4B51F4DE7630055351C /* LDCommon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDCommon.swift; sourceTree = ""; }; 83B6E3F0222EFA3800FF2A6A /* ThreadSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSpec.swift; sourceTree = ""; }; @@ -388,15 +411,29 @@ 83DDBEF51FA24A7E00E428B6 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; 83DDBEFD1FA24F9600E428B6 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; 83DDBEFF1FA2589900E428B6 /* FlagStoreSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagStoreSpec.swift; sourceTree = ""; }; - 83E2E2051F9E7AC7007514E9 /* LDUserSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDUserSpec.swift; sourceTree = ""; }; 83EBCBB020D9C7B5003A7142 /* FlagCounterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagCounterSpec.swift; sourceTree = ""; }; 83EBCBB220DABE1B003A7142 /* FlagRequestTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagRequestTracker.swift; sourceTree = ""; }; 83EBCBB620DABE93003A7142 /* FlagRequestTrackerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagRequestTrackerSpec.swift; sourceTree = ""; }; 83EF67921F9945E800403126 /* EventSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventSpec.swift; sourceTree = ""; }; - 83EF67941F994BAD00403126 /* LDUserStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDUserStub.swift; sourceTree = ""; }; 83F0A5631FB5F33800550A95 /* LDConfigSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDConfigSpec.swift; sourceTree = ""; }; 83FEF8DC1F266742001CF12C /* FlagSynchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlagSynchronizer.swift; sourceTree = ""; }; 83FEF8DE1F2667E4001CF12C /* EventReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventReporter.swift; sourceTree = ""; }; + A31088142837DC0400184942 /* Reference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reference.swift; sourceTree = ""; }; + A31088152837DC0400184942 /* Kind.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Kind.swift; sourceTree = ""; }; + A31088162837DC0400184942 /* LDContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDContext.swift; sourceTree = ""; }; + A31088242837DCA900184942 /* LDContextSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDContextSpec.swift; sourceTree = ""; }; + A31088252837DCA900184942 /* ReferenceSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceSpec.swift; sourceTree = ""; }; + A31088262837DCA900184942 /* KindSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KindSpec.swift; sourceTree = ""; }; + A33A5F7928466D04000C29C7 /* LDContextStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDContextStub.swift; sourceTree = ""; }; + A349D0312926CA0600DD5DE9 /* UserAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserAttribute.swift; sourceTree = ""; }; + A349D0322926CA0600DD5DE9 /* LDUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDUser.swift; sourceTree = ""; }; + A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDUser.swift; sourceTree = ""; }; + A349D06B2926CB2600DD5DE9 /* LDUserSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDUserSpec.swift; sourceTree = ""; }; + A355845629281CF00023D8EE /* LDUserStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDUserStub.swift; sourceTree = ""; }; + A355845829281E610023D8EE /* LDUserToContextSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDUserToContextSpec.swift; sourceTree = ""; }; + A3570F5928527B8200CF241A /* LDContextCodableSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDContextCodableSpec.swift; sourceTree = ""; }; + A36EDFC72853883400D91B05 /* ObjcLDReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcLDReference.swift; sourceTree = ""; }; + A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDContext.swift; sourceTree = ""; }; A3799D4429033665008D4A8E /* ObjcLDApplicationInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcLDApplicationInfo.swift; sourceTree = ""; }; B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticCacheSpec.swift; sourceTree = ""; }; B4265EB024E7390C001CFD2C /* TestUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtil.swift; sourceTree = ""; }; @@ -419,7 +456,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A322B4D328BE4DBA00A212ED /* LDSwiftEventSource in Frameworks */, + A30EF4F328C24AB600CD220E /* LDSwiftEventSource in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -427,7 +464,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A322B4D128BE4DB200A212ED /* LDSwiftEventSource in Frameworks */, + A30EF4F128C24AAD00CD220E /* LDSwiftEventSource in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -435,7 +472,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A322B4CD28BE4D9E00A212ED /* LDSwiftEventSource in Frameworks */, + A30EF4ED28C24A9A00CD220E /* LDSwiftEventSource in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -454,7 +491,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A322B4CF28BE4DA800A212ED /* LDSwiftEventSource in Frameworks */, + A30EF4EF28C24AA400CD220E /* LDSwiftEventSource in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -601,13 +638,14 @@ 8354EFE61F263E4200C05156 /* Models */ = { isa = PBXGroup; children = ( + A349D0322926CA0600DD5DE9 /* LDUser.swift */, + A349D0312926CA0600DD5DE9 /* UserAttribute.swift */, + A31088132837DC0400184942 /* Context */, C408884823033B7500420721 /* ConnectionInformation.swift */, B4C9D42D2489B5FF004A9B03 /* DiagnosticEvent.swift */, 8354EFDE1F26380700C05156 /* Event.swift */, 83EBCB9D20D9A0A1003A7142 /* FeatureFlag */, 8354EFDD1F26380700C05156 /* LDConfig.swift */, - 83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */, - 29A4C47427DA6266005B8D34 /* UserAttribute.swift */, ); path = Models; sourceTree = ""; @@ -615,12 +653,14 @@ 835E1D341F63332C00184DB4 /* ObjectiveC */ = { isa = PBXGroup; children = ( + A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */, + A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */, 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */, 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */, - 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */, 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */, B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */, 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */, + A36EDFC72853883400D91B05 /* ObjcLDReference.swift */, A3799D4429033665008D4A8E /* ObjcLDApplicationInfo.swift */, ); path = ObjectiveC; @@ -638,10 +678,11 @@ 83CFE7CF1F7AD89D0010544E /* Mocks */ = { isa = PBXGroup; children = ( + A355845629281CF00023D8EE /* LDUserStub.swift */, 83CFE7D01F7AD8DC0010544E /* DarklyServiceMock.swift */, 832307A71F7DA61B0029815A /* LDEventSourceMock.swift */, 832307A91F7ECA630029815A /* LDConfigStub.swift */, - 83EF67941F994BAD00403126 /* LDUserStub.swift */, + A33A5F7928466D04000C29C7 /* LDContextStub.swift */, 838F96791FBA551A009CFC45 /* ClientServiceMockFactory.swift */, 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */, 831425AE206ABB5300F2EF36 /* EnvironmentReportingMock.swift */, @@ -690,14 +731,6 @@ path = FlagChange; sourceTree = ""; }; - 83EBCBA620D9A23E003A7142 /* User */ = { - isa = PBXGroup; - children = ( - 83E2E2051F9E7AC7007514E9 /* LDUserSpec.swift */, - ); - path = User; - sourceTree = ""; - }; 83EBCBA720D9A251003A7142 /* FeatureFlag */ = { isa = PBXGroup; children = ( @@ -728,8 +761,9 @@ 83EF67911F9945CE00403126 /* Models */ = { isa = PBXGroup; children = ( + A349D06A2926CB1100DD5DE9 /* User */, + A31088232837DCA900184942 /* Context */, 83F0A5631FB5F33800550A95 /* LDConfigSpec.swift */, - 83EBCBA620D9A23E003A7142 /* User */, 83EBCBA720D9A251003A7142 /* FeatureFlag */, 83EF67921F9945E800403126 /* EventSpec.swift */, B4F689132497B2FC004D3CE0 /* DiagnosticEventSpec.swift */, @@ -757,6 +791,36 @@ path = ServiceObjects; sourceTree = ""; }; + A31088132837DC0400184942 /* Context */ = { + isa = PBXGroup; + children = ( + A31088142837DC0400184942 /* Reference.swift */, + A31088152837DC0400184942 /* Kind.swift */, + A31088162837DC0400184942 /* LDContext.swift */, + ); + path = Context; + sourceTree = ""; + }; + A31088232837DCA900184942 /* Context */ = { + isa = PBXGroup; + children = ( + A31088242837DCA900184942 /* LDContextSpec.swift */, + A31088252837DCA900184942 /* ReferenceSpec.swift */, + A31088262837DCA900184942 /* KindSpec.swift */, + A3570F5928527B8200CF241A /* LDContextCodableSpec.swift */, + ); + path = Context; + sourceTree = ""; + }; + A349D06A2926CB1100DD5DE9 /* User */ = { + isa = PBXGroup; + children = ( + A355845829281E610023D8EE /* LDUserToContextSpec.swift */, + A349D06B2926CB2600DD5DE9 /* LDUserSpec.swift */, + ); + path = User; + sourceTree = ""; + }; B467790E24D8AECA00897F00 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -820,7 +884,7 @@ ); name = LaunchDarkly_tvOS; packageProductDependencies = ( - A322B4D228BE4DBA00A212ED /* LDSwiftEventSource */, + A30EF4F228C24AB600CD220E /* LDSwiftEventSource */, ); productName = Darkly_tvOS; productReference = 831188382113A16900D77CB5 /* LaunchDarkly_tvOS.framework */; @@ -844,7 +908,7 @@ ); name = LaunchDarkly_macOS; packageProductDependencies = ( - A322B4D028BE4DB200A212ED /* LDSwiftEventSource */, + A30EF4F028C24AAD00CD220E /* LDSwiftEventSource */, ); productName = Darkly_macOS; productReference = 831EF33B20655D700001C643 /* LaunchDarkly_macOS.framework */; @@ -868,7 +932,7 @@ ); name = LaunchDarkly_iOS; packageProductDependencies = ( - A322B4CC28BE4D9E00A212ED /* LDSwiftEventSource */, + A30EF4EC28C24A9A00CD220E /* LDSwiftEventSource */, ); productName = Darkly; productReference = 8354EFC21F22491C00C05156 /* LaunchDarkly.framework */; @@ -917,7 +981,7 @@ ); name = LaunchDarkly_watchOS; packageProductDependencies = ( - A322B4CE28BE4DA800A212ED /* LDSwiftEventSource */, + A30EF4EE28C24AA400CD220E /* LDSwiftEventSource */, ); productName = "Darkly-watchOS"; productReference = 83D9EC6B2062DBB7004D7FA6 /* LaunchDarkly_watchOS.framework */; @@ -1110,18 +1174,23 @@ buildActionMask = 2147483647; files = ( 83906A7B21190B7700D7D3C5 /* DateFormatter.swift in Sources */, - 8311886A2113AE5D00D77CB5 /* ObjcLDUser.swift in Sources */, 831188502113ADEF00D77CB5 /* EnvironmentReporter.swift in Sources */, 831188682113AE5600D77CB5 /* ObjcLDClient.swift in Sources */, 831188572113AE0B00D77CB5 /* FlagChangeNotifier.swift in Sources */, 8311884D2113ADE200D77CB5 /* FlagsUnchangedObserver.swift in Sources */, 8311885F2113AE2D00D77CB5 /* HTTPURLRequest.swift in Sources */, + A36EDFD02853C50B00D91B05 /* ObjcLDContext.swift in Sources */, B4C9D4362489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, + A349D03A2926CA0600DD5DE9 /* LDUser.swift in Sources */, 831188452113ADC500D77CB5 /* LDClient.swift in Sources */, + A349D03F2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */, + A310881E2837DC0400184942 /* Kind.swift in Sources */, + A310881A2837DC0400184942 /* Reference.swift in Sources */, 831188522113ADF700D77CB5 /* KeyedValueCache.swift in Sources */, 831188582113AE0F00D77CB5 /* EventReporter.swift in Sources */, 8311885D2113AE2500D77CB5 /* DarklyService.swift in Sources */, 831188692113AE5900D77CB5 /* ObjcLDConfig.swift in Sources */, + A31088222837DC0400184942 /* LDContext.swift in Sources */, 8311886C2113AE6400D77CB5 /* ObjcLDChangedFlag.swift in Sources */, C43C37E8238DF22D003C1624 /* LDEvaluationDetail.swift in Sources */, 8311884C2113ADDE00D77CB5 /* FlagChangeObserver.swift in Sources */, @@ -1139,7 +1208,6 @@ 8311885A2113AE1500D77CB5 /* Log.swift in Sources */, 8311884B2113ADDA00D77CB5 /* LDChangedFlag.swift in Sources */, 8311885E2113AE2900D77CB5 /* HTTPURLResponse.swift in Sources */, - 29A4C47827DA6266005B8D34 /* UserAttribute.swift in Sources */, 8347BB0F21F147E100E56BCD /* LDTimer.swift in Sources */, B4C9D43B2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, C443A40523145FBF00145710 /* ConnectionInformation.swift in Sources */, @@ -1147,10 +1215,11 @@ 8354AC732243166900CDE602 /* FeatureFlagCache.swift in Sources */, 8311885B2113AE1D00D77CB5 /* Throttler.swift in Sources */, 8311884E2113ADE500D77CB5 /* Event.swift in Sources */, + A36EDFCB2853883400D91B05 /* ObjcLDReference.swift in Sources */, + A349D0362926CA0600DD5DE9 /* UserAttribute.swift in Sources */, 832D68A5224A38FC005F052A /* CacheConverter.swift in Sources */, 831188432113ADBE00D77CB5 /* LDCommon.swift in Sources */, B4C9D4312489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, - 831188462113ADCA00D77CB5 /* LDUser.swift in Sources */, 830DB3B12239B54900D65D25 /* URLResponse.swift in Sources */, 831188512113ADF400D77CB5 /* ClientServiceFactory.swift in Sources */, 831188442113ADC200D77CB5 /* LDConfig.swift in Sources */, @@ -1166,36 +1235,42 @@ buildActionMask = 2147483647; files = ( B468E71224B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, + A36EDFCF2853C50B00D91B05 /* ObjcLDContext.swift in Sources */, 831EF34320655E730001C643 /* LDCommon.swift in Sources */, 831EF34420655E730001C643 /* LDConfig.swift in Sources */, + A31088212837DC0400184942 /* LDContext.swift in Sources */, 831EF34520655E730001C643 /* LDClient.swift in Sources */, - 831EF34620655E730001C643 /* LDUser.swift in Sources */, 830DB3B02239B54900D65D25 /* URLResponse.swift in Sources */, B4C9D4352489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, 831EF34A20655E730001C643 /* FeatureFlag.swift in Sources */, C443A40C2315AA4D00145710 /* NetworkReporter.swift in Sources */, 831EF34B20655E730001C643 /* LDChangedFlag.swift in Sources */, + A36EDFCA2853883400D91B05 /* ObjcLDReference.swift in Sources */, 8354AC722243166900CDE602 /* FeatureFlagCache.swift in Sources */, + A310881D2837DC0400184942 /* Kind.swift in Sources */, C443A40423145FBE00145710 /* ConnectionInformation.swift in Sources */, 832D68A4224A38FC005F052A /* CacheConverter.swift in Sources */, 831EF34C20655E730001C643 /* FlagChangeObserver.swift in Sources */, 831EF34D20655E730001C643 /* FlagsUnchangedObserver.swift in Sources */, + A31088192837DC0400184942 /* Reference.swift in Sources */, 831EF34E20655E730001C643 /* Event.swift in Sources */, + A349D03E2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */, A3799D4729033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */, C443A41123186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, 831EF35020655E730001C643 /* ClientServiceFactory.swift in Sources */, 831EF35120655E730001C643 /* KeyedValueCache.swift in Sources */, 831AAE2E20A9E4F600B46DBA /* Throttler.swift in Sources */, 831EF35520655E730001C643 /* FlagSynchronizer.swift in Sources */, + A349D0392926CA0600DD5DE9 /* LDUser.swift in Sources */, B4C9D4302489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 831EF35620655E730001C643 /* FlagChangeNotifier.swift in Sources */, 831EF35720655E730001C643 /* EventReporter.swift in Sources */, 831EF35820655E730001C643 /* FlagStore.swift in Sources */, 831EF35920655E730001C643 /* Log.swift in Sources */, + A349D0352926CA0600DD5DE9 /* UserAttribute.swift in Sources */, 831EF35A20655E730001C643 /* HTTPHeaders.swift in Sources */, 831EF35B20655E730001C643 /* DarklyService.swift in Sources */, 831EF35C20655E730001C643 /* HTTPURLResponse.swift in Sources */, - 29A4C47727DA6266005B8D34 /* UserAttribute.swift in Sources */, C443A40723145FEE00145710 /* ConnectionInformationStore.swift in Sources */, 29FE129A280413D4008CC918 /* Util.swift in Sources */, 831EF35D20655E730001C643 /* HTTPURLRequest.swift in Sources */, @@ -1212,7 +1287,6 @@ 83B1D7C92073F354006D1B1C /* CwlSysctl.swift in Sources */, 831EF36620655E730001C643 /* ObjcLDClient.swift in Sources */, 831EF36720655E730001C643 /* ObjcLDConfig.swift in Sources */, - 831EF36820655E730001C643 /* ObjcLDUser.swift in Sources */, B4C9D43A2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, 831EF36A20655E730001C643 /* ObjcLDChangedFlag.swift in Sources */, ); @@ -1229,12 +1303,17 @@ 83FEF8DD1F266742001CF12C /* FlagSynchronizer.swift in Sources */, 830BF933202D188E006DF9B1 /* HTTPURLRequest.swift in Sources */, B4C9D4332489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, + A36EDFCD2853C50B00D91B05 /* ObjcLDContext.swift in Sources */, + A349D0372926CA0600DD5DE9 /* LDUser.swift in Sources */, 8354EFE51F263DAC00C05156 /* FeatureFlag.swift in Sources */, + A349D03C2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */, 8372668C20D4439600BD1088 /* DateFormatter.swift in Sources */, - 83A2D6241F51CD7A00EA3BD4 /* LDUser.swift in Sources */, + A310881B2837DC0400184942 /* Kind.swift in Sources */, + A31088172837DC0400184942 /* Reference.swift in Sources */, 8354EFE21F26380700C05156 /* Event.swift in Sources */, C408884923033B7500420721 /* ConnectionInformation.swift in Sources */, 831D8B721F71D3E700ED65E8 /* DarklyService.swift in Sources */, + A310881F2837DC0400184942 /* LDContext.swift in Sources */, 835E1D431F685AC900184DB4 /* ObjcLDChangedFlag.swift in Sources */, 8358F25E1F474E5900ECE1AF /* LDChangedFlag.swift in Sources */, 83D559741FD87CC9002D10C8 /* KeyedValueCache.swift in Sources */, @@ -1247,12 +1326,10 @@ 831D8B741F72994600ED65E8 /* FlagStore.swift in Sources */, 29F9D19E2812E005008D12C0 /* ObjcLDValue.swift in Sources */, 8358F2601F476AD800ECE1AF /* FlagChangeNotifier.swift in Sources */, - 835E1D411F63450A00184DB4 /* ObjcLDUser.swift in Sources */, 83FEF8DF1F2667E4001CF12C /* EventReporter.swift in Sources */, 831D2AAF2061AAA000B4AC3C /* Thread.swift in Sources */, 83B9A082204F6022000C3F17 /* FlagsUnchangedObserver.swift in Sources */, 8354EFE01F26380700C05156 /* LDClient.swift in Sources */, - 29A4C47527DA6266005B8D34 /* UserAttribute.swift in Sources */, 831425B1206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, C408884723033B3600420721 /* ConnectionInformationStore.swift in Sources */, 83B6C4B61F4DE7630055351C /* LDCommon.swift in Sources */, @@ -1260,7 +1337,9 @@ 8347BB0C21F147E100E56BCD /* LDTimer.swift in Sources */, B468E71024B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, 8354AC702243166900CDE602 /* FeatureFlagCache.swift in Sources */, + A36EDFC82853883400D91B05 /* ObjcLDReference.swift in Sources */, 8358F2621F47747F00ECE1AF /* FlagChangeObserver.swift in Sources */, + A349D0332926CA0600DD5DE9 /* UserAttribute.swift in Sources */, 832D68A2224A38FC005F052A /* CacheConverter.swift in Sources */, 835E1D401F63450A00184DB4 /* ObjcLDConfig.swift in Sources */, 83DDBEFE1FA24F9600E428B6 /* Date.swift in Sources */, @@ -1278,6 +1357,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A31088272837DCA900184942 /* LDContextSpec.swift in Sources */, 83CFE7CE1F7AD81D0010544E /* EventReporterSpec.swift in Sources */, 8392FFA32033565700320914 /* HTTPURLResponse.swift in Sources */, 83411A5F1FABDA8700E5CF39 /* mocks.generated.swift in Sources */, @@ -1288,32 +1368,37 @@ 83EF67931F9945E800403126 /* EventSpec.swift in Sources */, 837E38C921E804ED0008A50C /* EnvironmentReporterSpec.swift in Sources */, 83B6E3F1222EFA3800FF2A6A /* ThreadSpec.swift in Sources */, + A355845729281CF00023D8EE /* LDUserStub.swift in Sources */, 831AAE3020A9E75D00B46DBA /* ThrottlerSpec.swift in Sources */, 832D68AC224B3321005F052A /* CacheConverterSpec.swift in Sources */, 838F96741FB9F024009CFC45 /* LDClientSpec.swift in Sources */, - 83E2E2061F9E7AC7007514E9 /* LDUserSpec.swift in Sources */, 83A0E6B1203B557F00224298 /* FeatureFlagSpec.swift in Sources */, + A31088282837DCA900184942 /* ReferenceSpec.swift in Sources */, 83EBCBB720DABE93003A7142 /* FlagRequestTrackerSpec.swift in Sources */, B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */, B46F344125E6DB7D0078D45F /* DiagnosticReporterSpec.swift in Sources */, - 83EF67951F994BAD00403126 /* LDUserStub.swift in Sources */, B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */, 83F0A5641FB5F33800550A95 /* LDConfigSpec.swift in Sources */, 83CFE7D11F7AD8DC0010544E /* DarklyServiceMock.swift in Sources */, 832307AA1F7ECA630029815A /* LDConfigStub.swift in Sources */, + A33A5F7A28466D04000C29C7 /* LDContextStub.swift in Sources */, 8354AC77224316F800CDE602 /* FeatureFlagCacheSpec.swift in Sources */, 83EBCBB120D9C7B5003A7142 /* FlagCounterSpec.swift in Sources */, 83B8C2451FE360CF0082B8A9 /* FlagChangeNotifierSpec.swift in Sources */, 8335299E1FC37727001166F8 /* FlagMaintainingMock.swift in Sources */, 83383A5120460DD30024D975 /* SynchronizingErrorSpec.swift in Sources */, + A355845929281E610023D8EE /* LDUserToContextSpec.swift in Sources */, 83B9A080204F56F4000C3F17 /* FlagChangeObserverSpec.swift in Sources */, 830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */, 831425AF206ABB5300F2EF36 /* EnvironmentReportingMock.swift in Sources */, 838AB53F1F72A7D5006F03F5 /* FlagSynchronizerSpec.swift in Sources */, + A3570F5A28527B8200CF241A /* LDContextCodableSpec.swift in Sources */, 837406D421F760640087B22B /* LDTimerSpec.swift in Sources */, 832307A61F7D8D720029815A /* URLRequestSpec.swift in Sources */, 832307A81F7DA61B0029815A /* LDEventSourceMock.swift in Sources */, + A355845529281CD70023D8EE /* LDUserSpec.swift in Sources */, 838F967A1FBA551A009CFC45 /* ClientServiceMockFactory.swift in Sources */, + A31088292837DCA900184942 /* KindSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1325,15 +1410,20 @@ 83D9EC762062DEAB004D7FA6 /* LDConfig.swift in Sources */, 83EBCBB420DABE1B003A7142 /* FlagRequestTracker.swift in Sources */, 83D9EC772062DEAB004D7FA6 /* LDClient.swift in Sources */, - 83D9EC782062DEAB004D7FA6 /* LDUser.swift in Sources */, B4C9D4342489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, 83D9EC7C2062DEAB004D7FA6 /* FeatureFlag.swift in Sources */, + A36EDFCE2853C50B00D91B05 /* ObjcLDContext.swift in Sources */, 8372668D20D4439600BD1088 /* DateFormatter.swift in Sources */, + A349D0382926CA0600DD5DE9 /* LDUser.swift in Sources */, 83D9EC7D2062DEAB004D7FA6 /* LDChangedFlag.swift in Sources */, + A349D03D2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */, + A310881C2837DC0400184942 /* Kind.swift in Sources */, + A31088182837DC0400184942 /* Reference.swift in Sources */, 83D9EC7E2062DEAB004D7FA6 /* FlagChangeObserver.swift in Sources */, 83D9EC7F2062DEAB004D7FA6 /* FlagsUnchangedObserver.swift in Sources */, 83D9EC802062DEAB004D7FA6 /* Event.swift in Sources */, 83D9EC822062DEAB004D7FA6 /* ClientServiceFactory.swift in Sources */, + A31088202837DC0400184942 /* LDContext.swift in Sources */, 83D9EC832062DEAB004D7FA6 /* KeyedValueCache.swift in Sources */, 831AAE2D20A9E4F600B46DBA /* Throttler.swift in Sources */, C43C37E6238DF22B003C1624 /* LDEvaluationDetail.swift in Sources */, @@ -1351,7 +1441,6 @@ 83D9EC8D2062DEAB004D7FA6 /* DarklyService.swift in Sources */, 83D9EC8E2062DEAB004D7FA6 /* HTTPURLResponse.swift in Sources */, 83D9EC8F2062DEAB004D7FA6 /* HTTPURLRequest.swift in Sources */, - 29A4C47627DA6266005B8D34 /* UserAttribute.swift in Sources */, 831425B2206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, 83D9EC922062DEAB004D7FA6 /* Data.swift in Sources */, 8347BB0D21F147E100E56BCD /* LDTimer.swift in Sources */, @@ -1359,6 +1448,8 @@ C443A40323145FB700145710 /* ConnectionInformation.swift in Sources */, B4C9D4392489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, B468E71124B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, + A36EDFC92853883400D91B05 /* ObjcLDReference.swift in Sources */, + A349D0342926CA0600DD5DE9 /* UserAttribute.swift in Sources */, 83D9EC952062DEAB004D7FA6 /* Date.swift in Sources */, 832D68A3224A38FC005F052A /* CacheConverter.swift in Sources */, 83D9EC972062DEAB004D7FA6 /* Thread.swift in Sources */, @@ -1367,7 +1458,6 @@ 83D9EC992062DEAB004D7FA6 /* ObjcLDConfig.swift in Sources */, 830DB3AF2239B54900D65D25 /* URLResponse.swift in Sources */, A3799D4629033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */, - 83D9EC9A2062DEAB004D7FA6 /* ObjcLDUser.swift in Sources */, B495A8A32787762C0051977C /* LDClientVariation.swift in Sources */, 83D9EC9C2062DEAB004D7FA6 /* ObjcLDChangedFlag.swift in Sources */, ); @@ -1413,7 +1503,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 7.1.0; + MARKETING_VERSION = 8.0.0; MODULEMAP_FILE = ""; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-tvOS"; PRODUCT_NAME = LaunchDarkly_tvOS; @@ -1436,7 +1526,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 7.1.0; + MARKETING_VERSION = 8.0.0; MODULEMAP_FILE = ""; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-tvOS"; PRODUCT_NAME = LaunchDarkly_tvOS; @@ -1459,7 +1549,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 7.1.0; + MARKETING_VERSION = 8.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-macOS"; PRODUCT_NAME = LaunchDarkly_macOS; SDKROOT = macosx; @@ -1480,7 +1570,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 7.1.0; + MARKETING_VERSION = 8.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-macOS"; PRODUCT_NAME = LaunchDarkly_macOS; SDKROOT = macosx; @@ -1523,11 +1613,11 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; - DYLIB_COMPATIBILITY_VERSION = 7.1.0; - DYLIB_CURRENT_VERSION = 7.1.0; + DYLIB_COMPATIBILITY_VERSION = 8.0.0; + DYLIB_CURRENT_VERSION = 8.0.0; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - FRAMEWORK_VERSION = D; + FRAMEWORK_VERSION = E; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -1594,11 +1684,11 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DYLIB_COMPATIBILITY_VERSION = 7.1.0; - DYLIB_CURRENT_VERSION = 7.1.0; + DYLIB_COMPATIBILITY_VERSION = 8.0.0; + DYLIB_CURRENT_VERSION = 8.0.0; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - FRAMEWORK_VERSION = D; + FRAMEWORK_VERSION = E; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -1634,7 +1724,7 @@ INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_DYLIB_INSTALL_NAME = "$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)"; - MARKETING_VERSION = 7.1.0; + MARKETING_VERSION = 8.0.0; MODULEMAP_FILE = "$(PROJECT_DIR)/Framework/module.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.Darkly; PRODUCT_NAME = LaunchDarkly; @@ -1654,7 +1744,7 @@ INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_DYLIB_INSTALL_NAME = "$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)"; - MARKETING_VERSION = 7.1.0; + MARKETING_VERSION = 8.0.0; MODULEMAP_FILE = "$(PROJECT_DIR)/Framework/module.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.Darkly; PRODUCT_NAME = LaunchDarkly; @@ -1696,7 +1786,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 7.1.0; + MARKETING_VERSION = 8.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-watchOS"; PRODUCT_NAME = LaunchDarkly_watchOS; SDKROOT = watchos; @@ -1718,7 +1808,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 7.1.0; + MARKETING_VERSION = 8.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-watchOS"; PRODUCT_NAME = LaunchDarkly_watchOS; SDKROOT = watchos; @@ -1822,22 +1912,22 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - A322B4CC28BE4D9E00A212ED /* LDSwiftEventSource */ = { + A30EF4EC28C24A9A00CD220E /* LDSwiftEventSource */ = { isa = XCSwiftPackageProductDependency; package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; productName = LDSwiftEventSource; }; - A322B4CE28BE4DA800A212ED /* LDSwiftEventSource */ = { + A30EF4EE28C24AA400CD220E /* LDSwiftEventSource */ = { isa = XCSwiftPackageProductDependency; package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; productName = LDSwiftEventSource; }; - A322B4D028BE4DB200A212ED /* LDSwiftEventSource */ = { + A30EF4F028C24AAD00CD220E /* LDSwiftEventSource */ = { isa = XCSwiftPackageProductDependency; package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; productName = LDSwiftEventSource; }; - A322B4D228BE4DBA00A212ED /* LDSwiftEventSource */ = { + A30EF4F228C24AB600CD220E /* LDSwiftEventSource */ = { isa = XCSwiftPackageProductDependency; package = B445A6DE24C0D1CD000BAD6D /* XCRemoteSwiftPackageReference "swift-eventsource" */; productName = LDSwiftEventSource; diff --git a/LaunchDarkly.xcworkspace/contents.xcworkspacedata b/LaunchDarkly.xcworkspace/contents.xcworkspacedata index ad05ac86..8c28d75a 100644 --- a/LaunchDarkly.xcworkspace/contents.xcworkspacedata +++ b/LaunchDarkly.xcworkspace/contents.xcworkspacedata @@ -1,6 +1,9 @@ + + diff --git a/LaunchDarkly.xcworkspace/xcshareddata/xcschemes/ContractTests.xcscheme b/LaunchDarkly.xcworkspace/xcshareddata/xcschemes/ContractTests.xcscheme new file mode 100644 index 00000000..388c31da --- /dev/null +++ b/LaunchDarkly.xcworkspace/xcshareddata/xcschemes/ContractTests.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index 7fc8ce00..4a0ba753 100644 --- a/LaunchDarkly/GeneratedCode/mocks.generated.swift +++ b/LaunchDarkly/GeneratedCode/mocks.generated.swift @@ -12,10 +12,10 @@ final class CacheConvertingMock: CacheConverting { var convertCacheDataCallCount = 0 var convertCacheDataCallback: (() throws -> Void)? - var convertCacheDataReceivedArguments: (serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int)? - func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int) { + var convertCacheDataReceivedArguments: (serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedContexts: Int)? + func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedContexts: Int) { convertCacheDataCallCount += 1 - convertCacheDataReceivedArguments = (serviceFactory: serviceFactory, keysToConvert: keysToConvert, maxCachedUsers: maxCachedUsers) + convertCacheDataReceivedArguments = (serviceFactory: serviceFactory, keysToConvert: keysToConvert, maxCachedContexts: maxCachedContexts) try! convertCacheDataCallback?() } } @@ -128,15 +128,6 @@ final class EnvironmentReportingMock: EnvironmentReporting { } } - var deviceModelSetCount = 0 - var setDeviceModelCallback: (() throws -> Void)? - var deviceModel: String = Constants.deviceModel { - didSet { - deviceModelSetCount += 1 - try! setDeviceModelCallback?() - } - } - var systemVersionSetCount = 0 var setSystemVersionCallback: (() throws -> Void)? var systemVersion: String = Constants.systemVersion { @@ -224,7 +215,7 @@ final class EventReportingMock: EventReporting { var lastEventResponseDateSetCount = 0 var setLastEventResponseDateCallback: (() throws -> Void)? - var lastEventResponseDate: Date? = nil { + var lastEventResponseDate: Date = Date.distantPast { didSet { lastEventResponseDateSetCount += 1 try! setLastEventResponseDateCallback?() @@ -242,10 +233,10 @@ final class EventReportingMock: EventReporting { var recordFlagEvaluationEventsCallCount = 0 var recordFlagEvaluationEventsCallback: (() throws -> Void)? - var recordFlagEvaluationEventsReceivedArguments: (flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool)? - func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) { + var recordFlagEvaluationEventsReceivedArguments: (flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, context: LDContext, includeReason: Bool)? + func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, context: LDContext, includeReason: Bool) { recordFlagEvaluationEventsCallCount += 1 - recordFlagEvaluationEventsReceivedArguments = (flagKey: flagKey, value: value, defaultValue: defaultValue, featureFlag: featureFlag, user: user, includeReason: includeReason) + recordFlagEvaluationEventsReceivedArguments = (flagKey: flagKey, value: value, defaultValue: defaultValue, featureFlag: featureFlag, context: context, includeReason: includeReason) try! recordFlagEvaluationEventsCallback?() } @@ -273,21 +264,21 @@ final class FeatureFlagCachingMock: FeatureFlagCaching { var retrieveFeatureFlagsCallCount = 0 var retrieveFeatureFlagsCallback: (() throws -> Void)? - var retrieveFeatureFlagsReceivedUserKey: String? + var retrieveFeatureFlagsReceivedContextKey: String? var retrieveFeatureFlagsReturnValue: StoredItems? - func retrieveFeatureFlags(userKey: String) -> StoredItems? { + func retrieveFeatureFlags(contextKey: String) -> StoredItems? { retrieveFeatureFlagsCallCount += 1 - retrieveFeatureFlagsReceivedUserKey = userKey + retrieveFeatureFlagsReceivedContextKey = contextKey try! retrieveFeatureFlagsCallback?() return retrieveFeatureFlagsReturnValue } var storeFeatureFlagsCallCount = 0 var storeFeatureFlagsCallback: (() throws -> Void)? - var storeFeatureFlagsReceivedArguments: (storedItems: StoredItems, userKey: String, lastUpdated: Date)? - func storeFeatureFlags(_ storedItems: StoredItems, userKey: String, lastUpdated: Date) { + var storeFeatureFlagsReceivedArguments: (storedItems: StoredItems, contextKey: String, lastUpdated: Date)? + func storeFeatureFlags(_ storedItems: StoredItems, contextKey: String, lastUpdated: Date) { storeFeatureFlagsCallCount += 1 - storeFeatureFlagsReceivedArguments = (storedItems: storedItems, userKey: userKey, lastUpdated: lastUpdated) + storeFeatureFlagsReceivedArguments = (storedItems: storedItems, contextKey: contextKey, lastUpdated: lastUpdated) try! storeFeatureFlagsCallback?() } } diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 3e5ce38a..8bfd4492 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -5,13 +5,13 @@ enum LDClientRunMode { } /** - The LDClient is the heart of the SDK, providing client apps running iOS, watchOS, macOS, or tvOS access to LaunchDarkly services. This singleton provides the ability to set a configuration (LDConfig) that controls how the LDClient talks to LaunchDarkly servers, and a user (LDUser) that provides finer control on the feature flag values delivered to LDClient. Once the LDClient has started, it connects to LaunchDarkly's servers to get the feature flag values you set in the Dashboard. + The LDClient is the heart of the SDK, providing client apps running iOS, watchOS, macOS, or tvOS access to LaunchDarkly services. This singleton provides the ability to set a configuration (LDConfig) that controls how the LDClient talks to LaunchDarkly servers, and a contexts (LDContext) that provides finer control on the feature flag values delivered to LDClient. Once the LDClient has started, it connects to LaunchDarkly's servers to get the feature flag values you set in the Dashboard. ## Usage ### Startup - 1. To customize, configure a `LDConfig` and `LDUser`. The `config` is required, the `user` is optional. Both give you additional control over the feature flags delivered to the LDClient. See `LDConfig` & `LDUser` for more details. + 1. To customize, configure a `LDConfig` and `LDContext`. The `config` is required, the `context` is optional. Both give you additional control over the feature flags delivered to the LDClient. See `LDConfig` & `LDContext` for more details. - The mobileKey set into the `LDConfig` comes from your LaunchDarkly Account settings. If you have multiple projects be sure to choose the correct Mobile key. - 2. Call `LDClient.start(config: user: completion:)` - - If you do not pass in a LDUser, LDClient will create a default for you. + 2. Call `LDClient.start(config: context: completion:)` + - If you do not pass in a LDContext, LDClient will create a default for you. - The optional completion closure allows the LDClient to notify your app when it received flag values. 3. Because LDClient instances are stored statically, you do not have to keep a reference to it in your code. Get the primary instances with `LDClient.get()` @@ -91,7 +91,7 @@ public class LDClient { When offline, the SDK does not attempt to communicate with LaunchDarkly servers. Client apps can request feature flag values and set/change feature flag observers while offline. The SDK will collect events while offline. - The SDK protects itself from multiple rapid calls to setOnline(true) by enforcing an increasing delay (called *throttling*) each time setOnline(true) is called within a short time. The first time, the call proceeds normally. For each subsequent call the delay is enforced, and if waiting, increased to a maximum delay. When the delay has elapsed, the `setOnline(true)` will proceed, assuming that the client app has not called `setOnline(false)` during the delay. Therefore a call to setOnline(true) may not immediately result in the LDClient going online. Client app developers should consider this situation abnormal, and take steps to prevent the client app from making multiple rapid setOnline(true) calls. Calls to setOnline(false) are not throttled. Note that calls to `start(config: user: completion:)`, and setting the `config` or `user` can also call `setOnline(true)` under certain conditions. After the delay, the SDK resets and the client app can make a susequent call to setOnline(true) without being throttled. + The SDK protects itself from multiple rapid calls to setOnline(true) by enforcing an increasing delay (called *throttling*) each time setOnline(true) is called within a short time. The first time, the call proceeds normally. For each subsequent call the delay is enforced, and if waiting, increased to a maximum delay. When the delay has elapsed, the `setOnline(true)` will proceed, assuming that the client app has not called `setOnline(false)` during the delay. Therefore a call to setOnline(true) may not immediately result in the LDClient going online. Client app developers should consider this situation abnormal, and take steps to prevent the client app from making multiple rapid setOnline(true) calls. Calls to setOnline(false) are not throttled. Note that calls to `start(config: context: completion:)`, and setting the `config` or `context` can also call `setOnline(true)` under certain conditions. After the delay, the SDK resets and the client app can make a susequent call to setOnline(true) without being throttled. Client apps can set a completion closure called when the setOnline call completes. For unthrottled `setOnline(true)` and all `setOnline(false)` calls, the SDK will call the closure immediately on completion of this method. For throttled `setOnline(true)` calls, the SDK will call the closure after the throttling delay at the completion of the setOnline method. @@ -131,7 +131,7 @@ public class LDClient { private let internalSetOnlineQueue: DispatchQueue = DispatchQueue(label: "InternalSetOnlineQueue") - private func go(online goOnline: Bool, reasonOnlineUnavailable: String, completion:(() -> Void)?) { + private func go(online goOnline: Bool, reasonOnlineUnavailable: String, completion: (() -> Void)?) { let owner = "SetOnlineOwner" as AnyObject var completed = false let internalCompletedQueue = DispatchQueue(label: "com.launchdarkly.LDClient.goCompletedQueue") @@ -261,42 +261,55 @@ public class LDClient { let config: LDConfig let service: DarklyServiceProvider - private(set) var user: LDUser + private(set) var context: LDContext /** - The LDUser set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the user. See `LDUser` for details about what information can be retained. + The LDContext set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the context. See `LDContext` for details about what information can be retained. - Normally, the client app should create and set the LDUser and pass that into `start(config: user: completion:)`. + Normally, the client app should create and set the LDContext and pass that into `start(config: context: completion:)`. - The client app can change the active `user` by calling identify with a new or updated LDUser. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. + The client app can change the active `context` by calling identify with a new or updated LDContext. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. - When a new user is set, the LDClient goes offline and sets the new user. If the client was online when the new user was set, it goes online again, subject to a throttling delay if in force (see `setOnline(_: completion:)` for details). A completion may be passed to the identify method to allow a client app to know when fresh flag values for the new user are ready. + When a new context is set, the LDClient goes offline and sets the new context. If the client was online when the new context was set, it goes online again, subject to a throttling delay if in force (see `setOnline(_: completion:)` for details). A completion may be passed to the identify method to allow a client app to know when fresh flag values for the new context are ready. - - parameter user: The LDUser set with the desired user. + - parameter context: The LDContext set with the desired context. - parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays. (Optional) */ - public func identify(user: LDUser, completion: (() -> Void)? = nil) { + public func identify(context: LDContext, completion: (() -> Void)? = nil) { let dispatch = DispatchGroup() LDClient.instances?.forEach { _, instance in dispatch.enter() - instance.internalIdentify(newUser: user, completion: dispatch.leave) + instance.internalIdentify(newContext: context, completion: dispatch.leave) } if let completion = completion { dispatch.notify(queue: DispatchQueue.global(), execute: completion) } } - func internalIdentify(newUser: LDUser, completion: (() -> Void)? = nil) { + /** + Deprecated identify method which accepts a legacy LDUser instead of an LDContext. + + This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `identify(context:completion:)` for details. + */ + public func identify(user: LDUser, completion: (() -> Void)? = nil) { + switch user.toContext() { + case .failure(let error): + Log.debug(self.typeName(and: #function) + "user created an invalid context: SDK identified context WILL NOT CHANGE: " + error.localizedDescription ) + case .success(let context): + identify(context: context, completion: completion) + } + } + + func internalIdentify(newContext: LDContext, completion: (() -> Void)? = nil) { internalIdentifyQueue.sync { - let previousUser = self.user - self.user = newUser - Log.debug(self.typeName(and: #function) + "new user set with key: " + self.user.key ) + self.context = newContext + Log.debug(self.typeName(and: #function) + "new context set with key: " + self.context.fullyQualifiedKey() ) let wasOnline = self.isOnline self.internalSetOnline(false) - let cachedUserFlags = self.flagCache.retrieveFeatureFlags(userKey: self.user.key) ?? [:] - flagStore.replaceStore(newStoredItems: cachedUserFlags) - self.service.user = self.user + let cachedContextFlags = self.flagCache.retrieveFeatureFlags(contextKey: self.context.fullyQualifiedHashedKey()) ?? [:] + flagStore.replaceStore(newStoredItems: cachedContextFlags) + self.service.context = self.context self.service.clearFlagResponseCache() flagSynchronizer = serviceFactory.makeFlagSynchronizer(streamingMode: ConnectionInformation.effectiveStreamingMode(config: config, ldClient: self), pollingInterval: config.flagPollingInterval(runMode: runMode), @@ -305,14 +318,10 @@ public class LDClient { onSyncComplete: self.onFlagSyncComplete) if self.hasStarted { - self.eventReporter.record(IdentifyEvent(user: self.user)) + self.eventReporter.record(IdentifyEvent(context: self.context)) } self.internalSetOnline(wasOnline, completion: completion) - - if !config.autoAliasingOptOut && previousUser.isAnonymous && !newUser.isAnonymous { - self.alias(context: newUser, previousContext: previousUser) - } } } @@ -478,17 +487,17 @@ public class LDClient { let oldStoredItems = flagStore.storedItems connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) flagStore.replaceStore(newStoredItems: StoredItems(items: flagCollection.flags)) - self.updateCacheAndReportChanges(user: self.user, oldStoredItems: oldStoredItems) + self.updateCacheAndReportChanges(context: self.context, oldStoredItems: oldStoredItems) case let .patch(featureFlag): let oldStoredItems = flagStore.storedItems connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) flagStore.updateStore(updatedFlag: featureFlag) - self.updateCacheAndReportChanges(user: self.user, oldStoredItems: oldStoredItems) + self.updateCacheAndReportChanges(context: self.context, oldStoredItems: oldStoredItems) case let .delete(deleteResponse): let oldStoredItems = flagStore.storedItems connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) flagStore.deleteFlag(deleteResponse: deleteResponse) - self.updateCacheAndReportChanges(user: self.user, oldStoredItems: oldStoredItems) + self.updateCacheAndReportChanges(context: self.context, oldStoredItems: oldStoredItems) case .upToDate: connectionInformation.lastKnownFlagValidity = Date() flagChangeNotifier.notifyUnchanged() @@ -505,9 +514,9 @@ public class LDClient { connectionInformation = ConnectionInformation.synchronizingErrorCheck(synchronizingError: synchronizingError, connectionInformation: connectionInformation) } - private func updateCacheAndReportChanges(user: LDUser, + private func updateCacheAndReportChanges(context: LDContext, oldStoredItems: StoredItems) { - flagCache.storeFeatureFlags(flagStore.storedItems, userKey: user.key, lastUpdated: Date()) + flagCache.storeFeatureFlags(flagStore.storedItems, contextKey: context.fullyQualifiedHashedKey(), lastUpdated: Date()) flagChangeNotifier.notifyObservers(oldFlags: oldStoredItems.featureFlags, newFlags: flagStore.storedItems.featureFlags) } @@ -536,34 +545,11 @@ public class LDClient { Log.debug(typeName(and: #function) + "aborted. LDClient not started") return } - let event = CustomEvent(key: key, user: user, data: data ?? .null, metricValue: metricValue) + let event = CustomEvent(key: key, context: context, data: data ?? .null, metricValue: metricValue) Log.debug(typeName(and: #function) + "key: \(key), data: \(String(describing: data)), metricValue: \(String(describing: metricValue))") eventReporter.record(event) } - /** - Tells the SDK to generate an alias event. - - Associates two users for analytics purposes. - - This can be helpful in the situation where a person is represented by multiple - LaunchDarkly users. This may happen, for example, when a person initially logs into - an application-- the person might be represented by an anonymous user prior to logging - in and a different user after logging in, as denoted by a different user key. - - - parameter context: the user that will be aliased to - - parameter previousContext: the user that will be bound to the new context - */ - public func alias(context new: LDUser, previousContext old: LDUser) { - guard hasStarted - else { - Log.debug(typeName(and: #function) + "aborted. LDClient not started") - return - } - - self.eventReporter.record(AliasEvent(key: new.key, previousKey: old.key, contextKind: new.contextKind, previousContextKind: old.contextKind)) - } - /** Tells the SDK to immediately send any currently queued events to LaunchDarkly. @@ -596,21 +582,37 @@ public class LDClient { // MARK: Initializing and Accessing /** - Starts the LDClient using the passed in `config` & `user`. Call this before requesting feature flag values. The LDClient will not go online until you call this method. - Starting the LDClient means setting the `config` & `user`, setting the client online if `config.startOnline` is true (the default setting), and starting event recording. The client app must start the LDClient before it will report feature flag values. If a client does not call `start`, no methods will work. - If the `start` call omits the `user`, the LDClient uses a default `LDUser`. + Starts the LDClient using the passed in `config` & `context`. Call this before requesting feature flag values. The LDClient will not go online until you call this method. + Starting the LDClient means setting the `config` & `context`, setting the client online if `config.startOnline` is true (the default setting), and starting event recording. The client app must start the LDClient before it will report feature flag values. If a client does not call `start`, no methods will work. + If the `start` call omits the `context`, the LDClient uses a default `LDContext`. If the` start` call includes the optional `completion` closure, LDClient calls the `completion` closure when `setOnline(_: completion:)` embedded in the `init` method completes. This method listens for flag updates so the completion will only return once an update has occurred. The `start` call is subject to throttling delays, therefore the `completion` closure call may be delayed. - Subsequent calls to this method cause the LDClient to return. Normally there should only be one call to start. To change `user`, use `identify`. + Subsequent calls to this method cause the LDClient to return. Normally there should only be one call to start. To change `context`, use `identify`. - parameter configuration: The LDConfig that contains the desired configuration. (Required) - - parameter user: The LDUser set with the desired user. If omitted, LDClient sets a default user. (Optional) + - parameter context: The LDContext set with the desired context. If omitted, LDClient sets a default context. (Optional) - parameter completion: Closure called when the embedded `setOnline` call completes. (Optional) */ /// - Tag: start + public static func start(config: LDConfig, context: LDContext? = nil, completion: (() -> Void)? = nil) { + start(serviceFactory: nil, config: config, context: context, completion: completion) + } + + /** + Deprecated start method which accepts a legacy LDUser instead of an LDContext. + + This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `start(config:context:completion:)` for details. + */ public static func start(config: LDConfig, user: LDUser? = nil, completion: (() -> Void)? = nil) { - start(serviceFactory: nil, config: config, user: user, completion: completion) + switch user?.toContext() { + case nil: + start(serviceFactory: nil, config: config, context: nil, completion: completion) + case .failure(let error): + Log.debug(self.typeName(and: #function) + "user created an invalid context: " + error.localizedDescription ) + case .success(let context): + start(serviceFactory: nil, config: config, context: context, completion: completion) + } } - static func start(serviceFactory: ClientServiceCreating?, config: LDConfig, user: LDUser? = nil, completion: (() -> Void)? = nil) { + static func start(serviceFactory: ClientServiceCreating?, config: LDConfig, context: LDContext? = nil, completion: (() -> Void)? = nil) { Log.debug("LDClient starting") if serviceFactory != nil { get()?.close() @@ -623,7 +625,7 @@ public class LDClient { let serviceFactory = serviceFactory ?? ClientServiceFactory() var keys = [config.mobileKey] keys.append(contentsOf: config.getSecondaryMobileKeys().values) - serviceFactory.makeCacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: keys, maxCachedUsers: config.maxCachedUsers) + serviceFactory.makeCacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: keys, maxCachedContexts: config.maxCachedContexts) LDClient.instances = [:] var mobileKeys = config.getSecondaryMobileKeys() @@ -639,7 +641,7 @@ public class LDClient { for (name, mobileKey) in mobileKeys { var internalConfig = config internalConfig.mobileKey = mobileKey - let instance = LDClient(serviceFactory: serviceFactory, configuration: internalConfig, startUser: user, completion: completionCheck) + let instance = LDClient(serviceFactory: serviceFactory, configuration: internalConfig, startContext: context, completion: completionCheck) LDClient.instances?[name] = instance } completionCheck() @@ -649,23 +651,39 @@ public class LDClient { See [start](x-source-tag://start) for more information on starting the SDK. - parameter configuration: The LDConfig that contains the desired configuration. (Required) - - parameter user: The LDUser set with the desired user. If omitted, LDClient sets a default user. (Optional) + - parameter context: The LDContext set with the desired context. If omitted, LDClient sets a default context. (Optional) - parameter startWaitSeconds: A TimeInterval that determines when the completion will return if no flags have been returned from the network. - parameter completion: Closure called when the embedded `setOnline` call completes. Takes a Bool that indicates whether the completion timedout as a parameter. (Optional) */ + public static func start(config: LDConfig, context: LDContext? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { + start(serviceFactory: nil, config: config, context: context, startWaitSeconds: startWaitSeconds, completion: completion) + } + + /** + Deprecated start method which accepts a legacy LDUser instead of an LDContext. + + This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `start(config:context:startWaitSeconds:completion:)` for details. + */ public static func start(config: LDConfig, user: LDUser? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { - start(serviceFactory: nil, config: config, user: user, startWaitSeconds: startWaitSeconds, completion: completion) + switch user?.toContext() { + case nil: + start(serviceFactory: nil, config: config, context: nil, startWaitSeconds: startWaitSeconds, completion: completion) + case .failure(let error): + Log.debug(self.typeName(and: #function) + "user created an invalid context: " + error.localizedDescription ) + case .success(let context): + start(serviceFactory: nil, config: config, context: context, startWaitSeconds: startWaitSeconds, completion: completion) + } } - static func start(serviceFactory: ClientServiceCreating?, config: LDConfig, user: LDUser? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { + static func start(serviceFactory: ClientServiceCreating?, config: LDConfig, context: LDContext? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { var completed = true let internalCompletedQueue: DispatchQueue = DispatchQueue(label: "TimeOutQueue") if !config.startOnline { - start(serviceFactory: serviceFactory, config: config, user: user) + start(serviceFactory: serviceFactory, config: config, context: context) completion?(completed) } else { let startTime = Date().timeIntervalSince1970 - start(serviceFactory: serviceFactory, config: config, user: user) { + start(serviceFactory: serviceFactory, config: config, context: context) { internalCompletedQueue.async { if startTime + startWaitSeconds > Date().timeIntervalSince1970 && completed { completed = false @@ -720,18 +738,18 @@ public class LDClient { private var _initialized = false private var initializedQueue = DispatchQueue(label: "com.launchdarkly.LDClient.initializedQueue") - private init(serviceFactory: ClientServiceCreating, configuration: LDConfig, startUser: LDUser?, completion: (() -> Void)? = nil) { + private init(serviceFactory: ClientServiceCreating, configuration: LDConfig, startContext: LDContext?, completion: (() -> Void)? = nil) { self.serviceFactory = serviceFactory environmentReporter = self.serviceFactory.makeEnvironmentReporter() - flagCache = self.serviceFactory.makeFeatureFlagCache(mobileKey: configuration.mobileKey, maxCachedUsers: configuration.maxCachedUsers) + flagCache = self.serviceFactory.makeFeatureFlagCache(mobileKey: configuration.mobileKey, maxCachedContexts: configuration.maxCachedContexts) flagStore = self.serviceFactory.makeFlagStore() flagChangeNotifier = self.serviceFactory.makeFlagChangeNotifier() throttler = self.serviceFactory.makeThrottler(environmentReporter: environmentReporter) config = configuration - let anonymousUser = LDUser(environmentReporter: environmentReporter) - user = startUser ?? anonymousUser - service = self.serviceFactory.makeDarklyServiceProvider(config: config, user: user) + let anonymousContext = LDContext() + context = startContext ?? anonymousContext + service = self.serviceFactory.makeDarklyServiceProvider(config: config, context: context) diagnosticReporter = self.serviceFactory.makeDiagnosticReporter(service: service) eventReporter = self.serviceFactory.makeEventReporter(service: service) connectionInformation = self.serviceFactory.makeConnectionInformation() @@ -757,11 +775,11 @@ public class LDClient { onSyncComplete: onFlagSyncComplete) Log.level = environmentReporter.isDebugBuild && config.isDebugMode ? .debug : .noLogging - if let cachedFlags = flagCache.retrieveFeatureFlags(userKey: user.key), !cachedFlags.isEmpty { + if let cachedFlags = flagCache.retrieveFeatureFlags(contextKey: context.fullyQualifiedHashedKey()), !cachedFlags.isEmpty { flagStore.replaceStore(newStoredItems: cachedFlags) } - eventReporter.record(IdentifyEvent(user: user)) + eventReporter.record(IdentifyEvent(context: context)) self.connectionInformation = ConnectionInformation.uncacheConnectionInformation(config: config, ldClient: self, clientServiceFactory: self.serviceFactory) internalSetOnline(configuration.startOnline) { diff --git a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift index 42c55aff..95e9465b 100644 --- a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift +++ b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift @@ -8,7 +8,7 @@ extension LDClient { - parameter forKey: the unique feature key for the feature flag. - parameter defaultValue: the default value for if the flag value is unavailable. - - returns: the variation for the selected user, or `defaultValue` if the flag is not available. + - returns: the variation for the selected context, or `defaultValue` if the flag is not available. */ public func boolVariation(forKey flagKey: LDFlagKey, defaultValue: Bool) -> Bool { variationDetailInternal(flagKey, defaultValue, needsReason: false).value @@ -31,7 +31,7 @@ extension LDClient { - parameter forKey: the unique feature key for the feature flag. - parameter defaultValue: the default value for if the flag value is unavailable. - - returns: the variation for the selected user, or `defaultValue` if the flag is not available. + - returns: the variation for the selected context, or `defaultValue` if the flag is not available. */ public func intVariation(forKey flagKey: LDFlagKey, defaultValue: Int) -> Int { variationDetailInternal(flagKey, defaultValue, needsReason: false).value @@ -54,7 +54,7 @@ extension LDClient { - parameter forKey: the unique feature key for the feature flag. - parameter defaultValue: the default value for if the flag value is unavailable. - - returns: the variation for the selected user, or `defaultValue` if the flag is not available. + - returns: the variation for the selected context, or `defaultValue` if the flag is not available. */ public func doubleVariation(forKey flagKey: LDFlagKey, defaultValue: Double) -> Double { variationDetailInternal(flagKey, defaultValue, needsReason: false).value @@ -77,7 +77,7 @@ extension LDClient { - parameter forKey: the unique feature key for the feature flag. - parameter defaultValue: the default value for if the flag value is unavailable. - - returns: the variation for the selected user, or `defaultValue` if the flag is not available. + - returns: the variation for the selected context, or `defaultValue` if the flag is not available. */ public func stringVariation(forKey flagKey: LDFlagKey, defaultValue: String) -> String { variationDetailInternal(flagKey, defaultValue, needsReason: false).value @@ -100,7 +100,7 @@ extension LDClient { - parameter forKey: the unique feature key for the feature flag. - parameter defaultValue: the default value for if the flag value is unavailable. - - returns: the variation for the selected user, or `defaultValue` if the flag is not available. + - returns: the variation for the selected context, or `defaultValue` if the flag is not available. */ public func jsonVariation(forKey flagKey: LDFlagKey, defaultValue: LDValue) -> LDValue { variationDetailInternal(flagKey, defaultValue, needsReason: false).value @@ -137,7 +137,7 @@ extension LDClient { value: result.value.toLDValue(), defaultValue: defaultValue.toLDValue(), featureFlag: featureFlag, - user: user, + context: context, includeReason: needsReason) return result } diff --git a/LaunchDarkly/LaunchDarkly/LDCommon.swift b/LaunchDarkly/LaunchDarkly/LDCommon.swift index 1cfcefb1..6b65a128 100644 --- a/LaunchDarkly/LaunchDarkly/LDCommon.swift +++ b/LaunchDarkly/LaunchDarkly/LDCommon.swift @@ -49,7 +49,7 @@ struct DynamicKey: CodingKey { encoded internally as double-precision floating-point), a string, an ordered list of `LDValue` values (a JSON array), or a map of strings to `LDValue` values (a JSON object). - This can be used to represent complex data in a user custom attribute, or to get a feature flag value that uses a + This can be used to represent complex data in a context attribute, or to get a feature flag value that uses a complex type or does not always use the same type. */ public enum LDValue: Codable, diff --git a/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift b/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift index c80981b6..dfcf2883 100644 --- a/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift +++ b/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift @@ -109,7 +109,7 @@ public struct ConnectionInformation: Codable, CustomStringConvertible { connectionInformationVar.lastFailedConnection = Date() return connectionInformationVar } - + // This function is used to ensure we switch from establishing a streaming connection to streaming once we are connected. static func checkEstablishingStreaming(connectionInformation: ConnectionInformation) -> ConnectionInformation { var connectionInformationVar = connectionInformation diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift b/LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift new file mode 100644 index 00000000..45926d22 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift @@ -0,0 +1,91 @@ +import Foundation + +/// Kind is an enumeration set by the application to describe what kind of entity an `LDContext` +/// represents. The meaning of this is completely up to the application. When no Kind is +/// specified, the default is `Kind.user`. +/// +/// For a multi-context (see `LDMultiContextBuilder`), the Kind is always `Kind.multi`; +/// there is a specific Kind for each of the individual Contexts within it. +public enum Kind: Codable, Equatable, Hashable { + /// user is both the default Kind and also the kind used for legacy users in earlier versions of this SDK. + case user + + /// multi is only usable by constructing a multi-context using `LDMultiContextBuilder`. Attempting to set + /// a context kind to multi directly will result in an invalid context. + case multi + + /// The custom case handles arbitrarily defined contexts (e.g. org, account, server). + case custom(String) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + switch try container.decode(String.self) { + case "user": + self = .user + case "multi": + self = .multi + case let custom: + self = .custom(custom) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.description) + } + + internal func isMulti() -> Bool { + self == .multi || self == .custom("multi") + } + + internal func isUser() -> Bool { + self == .user || self == .custom("user") || self == .custom("") + } + + private static func isValid(_ description: String) -> Bool { + description.onlyContainsCharset(Util.validKindCharacterSet) + } +} + +extension Kind: Comparable { + public static func < (lhs: Kind, rhs: Kind) -> Bool { + lhs.description < rhs.description + } + + public static func == (lhs: Kind, rhs: Kind) -> Bool { + lhs.description == rhs.description + } +} + +extension Kind: LosslessStringConvertible { + public init?(_ description: String) { + switch description { + case "kind": + return nil + case "multi": + self = .multi + case "", "user": + self = .user + default: + if !Kind.isValid(description) { + return nil + } + + self = .custom(description) + } + } +} + +extension Kind: CustomStringConvertible { + public var description: String { + switch self { + case .user: + return "user" + case .multi: + return "multi" + case let .custom(val): + return val + } + } +} diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift new file mode 100644 index 00000000..59a38254 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -0,0 +1,909 @@ +import Foundation + +/// Enumeration representing various modes of failures when constructing an `LDContext`. +public enum ContextBuilderError: Error { + /// The provided kind either contains invalid characters, or is the disallowed kind "kind". + case invalidKind + /// The `LDMultiContextBuilder` must be used when attempting to build a multi-context. + case requiresMultiBuilder + /// The JSON representations for the context was missing the "key" property. + case emptyKey + /// Attempted to build a multi-context without providing any contexts. + case emptyMultiKind + /// A multi-context cannot contain another multi-context. + case nestedMultiKind + /// Attempted to build a multi-context containing 2 or more contexts with the same kind. + case duplicateKinds +} + +/// LDContext is a collection of attributes that can be referenced in flag evaluations and analytics +/// events. +/// +/// To create an LDContext of a single kind, such as a user, you may use `LDContextBuilder`. +/// +/// To create an LDContext with multiple kinds, use `LDMultiContextBuilder`. +public struct LDContext: Encodable, Equatable { + static let storedIdKey: String = "ldDeviceIdentifier" + + internal var kind: Kind = .user + fileprivate var contexts: [LDContext] = [] + + // Meta attributes + fileprivate var name: String? + fileprivate var anonymous: Bool = false + internal var privateAttributes: [Reference] = [] + + fileprivate var key: String? + fileprivate var canonicalizedKey: String + internal var attributes: [String: LDValue] = [:] + + fileprivate init(canonicalizedKey: String) { + self.canonicalizedKey = canonicalizedKey + } + + init() { + self.init(canonicalizedKey: LDContext.defaultKey(kind: Kind.user)) + } + + struct Meta: Codable { + var privateAttributes: [Reference]? + var redactedAttributes: [String]? + + enum CodingKeys: CodingKey { + case privateAttributes, redactedAttributes + } + + var isEmpty: Bool { + (privateAttributes?.isEmpty ?? true) + && (redactedAttributes?.isEmpty ?? true) + } + + init(privateAttributes: [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) + + self.privateAttributes = privateAttributes + self.redactedAttributes = [] + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + if let privateAttributes = privateAttributes, !privateAttributes.isEmpty { + try container.encodeIfPresent(privateAttributes, forKey: .privateAttributes) + } + + if let redactedAttributes = redactedAttributes, !redactedAttributes.isEmpty { + try container.encodeIfPresent(redactedAttributes, forKey: .redactedAttributes) + } + } + } + + class PrivateAttributeLookupNode { + var reference: Reference? + var children = SharedDictionary() + + init() { + self.reference = nil + } + + init(reference: Reference) { + self.reference = reference + } + } + + static private func encodeSingleContext(container: inout KeyedEncodingContainer, context: LDContext, discardKind: Bool, includePrivateAttributes: Bool, allAttributesPrivate: Bool, globalPrivateAttributes: SharedDictionary) throws { + if !discardKind { + try container.encodeIfPresent(context.kind.description, forKey: DynamicCodingKeys(string: "kind")) + } + + try container.encodeIfPresent(context.key, forKey: DynamicCodingKeys(string: "key")) + + let optionalAttributeNames = context.getOptionalAttributeNames() + var redactedAttributes: [String] = [] + redactedAttributes.reserveCapacity(20) + + for key in optionalAttributeNames { + let reference = Reference(key) + if let value = context.getValue(reference) { + if allAttributesPrivate { + redactedAttributes.append(reference.raw()) + continue + } + + var path: [String] = [] + path.reserveCapacity(10) + try LDContext.writeFilterAttribute(context: context, container: &container, parentPath: path, key: key, value: value, redactedAttributes: &redactedAttributes, includePrivateAttributes: includePrivateAttributes, globalPrivateAttributes: globalPrivateAttributes) + } + } + + let meta = Meta(privateAttributes: context.privateAttributes, redactedAttributes: redactedAttributes) + + if !meta.isEmpty { + try container.encodeIfPresent(meta, forKey: DynamicCodingKeys(string: "_meta")) + } + + if context.anonymous { + try container.encodeIfPresent(context.anonymous, forKey: DynamicCodingKeys(string: "anonymous")) + } + } + + static private func writeFilterAttribute(context: LDContext, container: inout KeyedEncodingContainer, parentPath: [String], key: String, value: LDValue, redactedAttributes: inout [String], includePrivateAttributes: Bool, globalPrivateAttributes: SharedDictionary) throws { + var path = parentPath + path.append(key.description) + + let (isReacted, nestedPropertiesAreRedacted) = includePrivateAttributes ? (false, false) : LDContext.maybeRedact(context: context, parentPath: path, value: value, redactedAttributes: &redactedAttributes, globalPrivateAttributes: globalPrivateAttributes) + + switch value { + case .object where isReacted: + break + case .object(let objectMap): + if !nestedPropertiesAreRedacted { + try container.encode(value, forKey: DynamicCodingKeys(string: key)) + return + } + + // TODO(mmk): This might be a problem. We might write a sub container even if all the attributes are completely filtered out. + var subContainer = container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: DynamicCodingKeys(string: key)) + for (key, value) in objectMap { + try writeFilterAttribute(context: context, container: &subContainer, parentPath: path, key: key, value: value, redactedAttributes: &redactedAttributes, includePrivateAttributes: includePrivateAttributes, globalPrivateAttributes: globalPrivateAttributes) + } + case _ where !isReacted: + try container.encode(value, forKey: DynamicCodingKeys(string: key)) + default: + break + } + } + + static private func maybeRedact(context: LDContext, parentPath: [String], value: LDValue, redactedAttributes: inout [String], globalPrivateAttributes: SharedDictionary) -> (Bool, Bool) { + var (reactedAttrReference, nestedPropertiesAreRedacted) = LDContext.checkGlobalPrivateAttributeReferences(context: context, parentPath: parentPath, globalPrivateAttributes: globalPrivateAttributes) + + if let reactedAttrReference = reactedAttrReference { + redactedAttributes.append(reactedAttrReference.raw()) + return (true, false) + } + + var shouldCheckNestedProperties: Bool = false + if case .object = value { + shouldCheckNestedProperties = true + } + + for privateAttribute in context.privateAttributes { + let depth = privateAttribute.depth() + + if depth < parentPath.count { + continue + } + + if !shouldCheckNestedProperties && depth < parentPath.count { + continue + } + + var hasMatch = true + for (index, parentPart) in parentPath.enumerated() { + if let name = privateAttribute.component(index) { + if name != parentPart { + hasMatch = false + break + } + + continue + } else { + break + } + } + + if hasMatch { + if depth == parentPath.count { + redactedAttributes.append(privateAttribute.raw()) + return (true, false) + } + + nestedPropertiesAreRedacted = true + } + } + + return (false, nestedPropertiesAreRedacted) + } + + static internal func defaultKey(kind: Kind) -> String { + // ldDeviceIdentifier is used for users to be compatible with + // older SDKs + let storedIdKey = kind.isUser() ? "ldDeviceIdentifier" : "ldGeneratedContextKey:\(kind)" + if let storedId = UserDefaults.standard.string(forKey: storedIdKey) { + return storedId + } + + let key = UUID().uuidString + UserDefaults.standard.set(key, forKey: storedIdKey) + + return key + } + + internal struct UserInfoKeys { + static let includePrivateAttributes = CodingUserInfoKey(rawValue: "LD_includePrivateAttributes")! + static let allAttributesPrivate = CodingUserInfoKey(rawValue: "LD_allAttributesPrivate")! + static let globalPrivateAttributes = CodingUserInfoKey(rawValue: "LD_globalPrivateAttributes")! + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: DynamicCodingKeys.self) + + let includePrivateAttributes = encoder.userInfo[UserInfoKeys.includePrivateAttributes] as? Bool ?? false + let allAttributesPrivate = encoder.userInfo[UserInfoKeys.allAttributesPrivate] as? Bool ?? false + let globalPrivateAttributes = encoder.userInfo[UserInfoKeys.globalPrivateAttributes] as? [Reference] ?? [] + + let allPrivate = !includePrivateAttributes && allAttributesPrivate + let globalPrivate = includePrivateAttributes ? [] : globalPrivateAttributes + let globalDictionary = LDContext.makePrivateAttributeLookupData(references: globalPrivate) + + if isMulti() { + try container.encodeIfPresent(kind.description, forKey: DynamicCodingKeys(string: "kind")) + + for context in contexts { + var contextContainer = container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: DynamicCodingKeys(string: context.kind.description)) + try LDContext.encodeSingleContext(container: &contextContainer, context: context, discardKind: true, includePrivateAttributes: includePrivateAttributes, allAttributesPrivate: allPrivate, globalPrivateAttributes: globalDictionary) + } + } else { + try LDContext.encodeSingleContext(container: &container, context: self, discardKind: false, includePrivateAttributes: includePrivateAttributes, allAttributesPrivate: allPrivate, globalPrivateAttributes: globalDictionary) + } + } + + class SharedDictionary { + private var dict: [K: V] = Dictionary() + var isEmpty: Bool { + dict.isEmpty + } + + func contains(_ key: K) -> Bool { + dict.keys.contains(key) + } + + subscript(key: K) -> V? { + get { return dict[key] } + set { dict[key] = newValue } + } + } + + static private func makePrivateAttributeLookupData(references: [Reference]) -> SharedDictionary { + let returnValue = SharedDictionary() + + for reference in references { + let parentMap = returnValue + + for index in 0...reference.depth() { + if let name = reference.component(index) { + if !parentMap.contains(name) { + let nextNode = PrivateAttributeLookupNode() + + if index == reference.depth() - 1 { + nextNode.reference = reference + } + + parentMap[name] = nextNode + } + } + } + } + + return returnValue + } + + static private func checkGlobalPrivateAttributeReferences(context: LDContext, parentPath: [String], globalPrivateAttributes: SharedDictionary) -> (Reference?, Bool) { + var lookup = globalPrivateAttributes + if lookup.isEmpty { + return (nil, false) + } + + for (index, path) in parentPath.enumerated() { + if let nextNode = lookup[path] { + if index == parentPath.count - 1 { + let name = (nextNode.reference, nextNode.reference == nil) + return name + } else if !nextNode.children.isEmpty { + lookup = nextNode.children + } + } else { + break + } + } + + return (nil, false) + } + + /// FullyQualifiedKey returns a string that describes the entire Context based on Kind and Key values. + /// + /// This value is used whenever LaunchDarkly needs a string identifier based on all of the Kind and + /// Key values in the context; the SDK may use this for caching previously seen contexts, for instance. + public func fullyQualifiedKey() -> String { + return canonicalizedKey + } + + func fullyQualifiedHashedKey() -> String { + if kind.isUser() { + return Util.sha256base64(fullyQualifiedKey()) + } + + return Util.sha256base64(fullyQualifiedKey()) + "$" + } + + /// - Returns: true if the `LDContext` is a multi-context; false otherwise. + public func isMulti() -> Bool { + return self.kind.isMulti() + } + + //// - Returns: A hash mapping a context's kind to its key. + public func contextKeys() -> [String: String] { + guard isMulti() else { + return [kind.description: key ?? ""] + } + + let keys = Dictionary(contexts.map { ($0.kind.description, $0.key ?? "") }) { first, _ in first } + return keys + } + + /// Looks up the value of any attribute of the `LDContext`, or a value contained within an + /// attribute, based on a `Reference`. This includes only attributes that are addressable in evaluations. + /// + /// This implements the same behavior that the SDK uses to resolve attribute references during a flag + /// evaluation. In a context, the `Reference` can represent a simple attribute name-- either a + /// built-in one like "name" or "key", or a custom attribute that was set by `LDContextBuilder.trySetValue(...)`-- + /// or, it can be a slash-delimited path using a JSON-Pointer-like syntax. See `Reference` for more details. + /// + /// For a multi-context, the only supported attribute name is "kind". + /// + /// If the value is found, the return value is the attribute value, using the type `LDValue` to + /// represent a value of any JSON type. + /// + /// If there is no such attribute, or if the `Reference` is invalid, the return value is nil. + public func getValue(_ reference: Reference) -> LDValue? { + if !reference.isValid() { + return nil + } + + guard let component = reference.component(0) else { + return nil + } + + if isMulti() { + if reference.depth() == 1 && component == "kind" { + return .string(String(kind)) + } + + Log.debug(typeName(and: #function) + ": Cannot get non-kind attribute from multi-context") + return nil + } + + guard var attribute: LDValue = self.getTopLevelAddressableAttributeSingleKind(component) else { + return nil + } + + for depth in 1.. [String] { + if isMulti() { + return [] + } + + var attrs = attributes.keys.map { $0.description } + + if name != nil { + attrs.append("name") + } + + return attrs + } + + func getTopLevelAddressableAttributeSingleKind(_ name: String) -> LDValue? { + switch name { + case "kind": + return .string(String(self.kind)) + case "key": + return self.key.map { .string($0) } + case "name": + return self.name.map { .string($0) } + case "anonymous": + return .bool(self.anonymous) + default: + return self.attributes[name] + } + } +} + +extension LDContext: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + switch try container.decodeIfPresent(String.self, forKey: DynamicCodingKeys(string: "kind")) { + case .none: + if container.contains(DynamicCodingKeys(string: "kind")) { + throw DecodingError.valueNotFound( + String.self, + DecodingError.Context( + codingPath: [DynamicCodingKeys(string: "kind")], + debugDescription: "Kind cannot be null" + ) + ) + } + + let values = try decoder.container(keyedBy: UserCodingKeys.self) + + let key = try values.decode(String.self, forKey: .key) + var contextBuilder = LDContextBuilder(key: key) + contextBuilder.allowEmptyKey = true + + let custom = try values.decodeIfPresent([String: LDValue].self, forKey: .custom) ?? [:] + custom.forEach { contextBuilder.trySetValue($0.key, $0.value) } + + if let name = try values.decodeIfPresent(String.self, forKey: .name) { + contextBuilder.name(name) + } + if let firstName = try values.decodeIfPresent(String.self, forKey: .firstName) { + contextBuilder.trySetValue("firstName", .string(firstName)) + } + if let lastName = try values.decodeIfPresent(String.self, forKey: .lastName) { + contextBuilder.trySetValue("lastName", .string(lastName)) + } + if let country = try values.decodeIfPresent(String.self, forKey: .country) { + contextBuilder.trySetValue("country", .string(country)) + } + if let ip = try values.decodeIfPresent(String.self, forKey: .ip) { + contextBuilder.trySetValue("ip", .string(ip)) + } + if let email = try values.decodeIfPresent(String.self, forKey: .email) { + contextBuilder.trySetValue("email", .string(email)) + } + if let avatar = try values.decodeIfPresent(String.self, forKey: .avatar) { + contextBuilder.trySetValue("avatar", .string(avatar)) + } + + let isAnonymous = try values.decodeIfPresent(Bool.self, forKey: .isAnonymous) ?? false + contextBuilder.anonymous(isAnonymous) + + let privateAttributeNames = try values.decodeIfPresent([String].self, forKey: .privateAttributeNames) ?? [] + privateAttributeNames.forEach { contextBuilder.addPrivateAttribute(Reference($0)) } + + self = try contextBuilder.build().get() + case .some("multi"): + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + var multiContextBuilder = LDMultiContextBuilder() + + for key in container.allKeys { + if key.stringValue == "kind" { + continue + } + + let contextContainer = try container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: DynamicCodingKeys(string: key.stringValue)) + multiContextBuilder.addContext(try LDContext.decodeSingleContext(container: contextContainer, kind: key.stringValue)) + } + + self = try multiContextBuilder.build().get() + case .some(""): + throw DecodingError.valueNotFound( + String.self, + DecodingError.Context( + codingPath: [DynamicCodingKeys(string: "kind")], + debugDescription: "Kind cannot be empty" + ) + ) + case .some(let kind): + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + self = try LDContext.decodeSingleContext(container: container, kind: kind) + } + } + + static private func decodeSingleContext(container: KeyedDecodingContainer, kind: String) throws -> LDContext { + let key = try container.decode(String.self, forKey: DynamicCodingKeys(string: "key")) + + var contextBuilder = LDContextBuilder(key: key) + contextBuilder.kind(kind) + + for key in container.allKeys { + switch key.stringValue { + case "key": + continue + case "_meta": + if let meta = try container.decodeIfPresent(LDContext.Meta.self, forKey: DynamicCodingKeys(string: "_meta")) { + if let privateAttributes = meta.privateAttributes { + privateAttributes.forEach { contextBuilder.addPrivateAttribute($0) } + } + } + + default: + if let value = try container.decodeIfPresent(LDValue.self, forKey: DynamicCodingKeys(string: key.stringValue)) { + contextBuilder.trySetValue(key.stringValue, value) + } + } + } + + return try contextBuilder.build().get() + } + + // This CodingKey implementation allows us to dynamically access fields in + // any JSON payload without having to pre-define the possible keys. + private struct DynamicCodingKeys: CodingKey { + // Protocol required implementations + var stringValue: String + var intValue: Int? + + init?(stringValue: String) { + self.stringValue = stringValue + } + + init?(intValue: Int) { + return nil + } + + // Convenience method since we don't want to unwrap everywhere + init(string: String) { + self.stringValue = string + } + } + + enum UserCodingKeys: String, CodingKey { + case key, name, firstName, lastName, country, ip, email, avatar, custom, isAnonymous = "anonymous", privateAttributeNames + } +} + +extension LDContext: TypeIdentifying {} + +enum LDContextBuilderKey { + case generateKey + case key(String) +} + +/// Contains methods for building a single kind `LDContext` with a specified key, defaulting to kind +/// "user". +/// +/// You may use these methods to set additional attributes and/or change the kind before calling +/// `LDContextBuilder.build()`. If you do not change any values, the defaults for the `LDContext` are that its +/// kind is "user", its key is set to whatever value you passed to `LDContextBuilder.init(key:)`, its anonymous attribute +/// is false, and it has no values for any other attributes. +/// +/// To define a multi-context, see `LDMultiContextBuilder`. +public struct LDContextBuilder { + private var kind: String = Kind.user.description + + // Meta attributes + private var name: String? + private var anonymous: Bool = false + private var privateAttributes: [Reference] = [] + + private var key: LDContextBuilderKey + private var attributes: [String: LDValue] = [:] + + // Contexts that were deserialized from implicit user formats + // are allowed to have empty string keys. Otherwise, key is + // never allowed to be empty. + fileprivate var allowEmptyKey: Bool = false + + /// Create a new LDContextBuilder. + /// + /// By default, this builder will create an anonymous LDContext + /// with a generated key. This key will be cached locally and + /// reused for the same context kind. + /// + /// If `LDContextBuilder.key` is called, a key will no longer be + /// generated and the anonymous status will match the value + /// provided by `LDContextBuilder.anonymous` or false by default. + public init() { + self.key = .generateKey + } + + /// Create a new LDContextBuilder with the provided `key`. + public init(key: String) { + self.key = .key(key) + } + + /// Sets the LDContext's kind attribute. + /// + /// Every LDContext has a kind. Setting it to an empty string is equivalent to the default kind + /// of "user". This value is case-sensitive. Validation rules are as follows: + /// + /// - It may only contain letters, numbers, and the characters ".", "_", and "-". + /// - It cannot equal the literal string "kind". + /// - It cannot equal "multi". + /// + /// If the value is invalid, you will receive an error when `LDContextBuilder.build()` is called. + public mutating func kind(_ kind: String) { + self.kind = kind + } + + /// Sets the LDContext's key attribute. + /// + /// Every LDContext has a key, which is always a string. There are no restrictions on its value. + /// It may be an empty string. + /// + /// The key attribute can be referenced by flag rules, flag target lists, and segments. + public mutating func key(_ key: String) { + self.key = .key(key) + } + + /// Sets the LDContext's name attribute. + /// + /// This attribute is optional. It has the following special rules: + /// + /// - Unlike most other attributes, it is always a string if it is specified. + /// - The LaunchDarkly dashboard treats this attribute as the preferred display name for users. + public mutating func name(_ name: String) { + self.name = name + } + + /// Sets the value of any attribute for the Context except for private attributes. + /// + /// This method uses the `LDValue` type to represent a value of any JSON type: null, + /// boolean, number, string, array, or object. For all attribute names that do not have special + /// meaning to LaunchDarkly, you may use any of those types. Values of different JSON types are + /// always treated as different values: for instance, null, false, and the empty string "" are + /// not the same, and the number 1 is not the same as the string "1". + /// + /// The following attribute names have special restrictions on their value types, and any value + /// of an unsupported type will be ignored (leaving the attribute unchanged): + /// + /// - "kind", "key": Must be a string. See `LDContextBuilder.kind(_:)` and `LDContextBuilder.key(_:)`. + /// + /// - "name": Must be a string or null. See `LDContextBuilder.name(_:)`. + /// + /// - "anonymous": Must be a boolean. See `LDContextBuilder.anonymous(_:)`. + /// + /// Values that are JSON arrays or objects have special behavior when referenced in + /// flag/segment rules. + /// + /// A value of `LDValue.null` is equivalent to removing any current non-default value + /// of the attribute. Null is not a valid attribute value in the LaunchDarkly model; any + /// expressions in feature flags that reference an attribute with a null value will behave as + /// if the attribute did not exist. + /// + /// This method returns true for success, or false if the parameters + /// violated one of the restrictions described above (for instance, + /// attempting to set "key" to a value that was not a string). + @discardableResult + public mutating func trySetValue(_ name: String, _ value: LDValue) -> Bool { + switch (name, value) { + case ("", _): + Log.debug(typeName(and: #function) + ": Provided attribute is empty. Ignoring.") + return false + case ("kind", .string(kind)): + self.kind(kind) + case ("kind", _): + return false + case ("key", .string(let val)): + self.key(val) + case ("key", _): + return false + case ("name", .string(let val)): + self.name(val) + case ("name", _): + return false + case ("anonymous", .bool(let val)): + self.anonymous(val) + case ("anonymous", _): + return false + case (_, .null): + self.attributes.removeValue(forKey: name) + case (_, _): + self.attributes.updateValue(value, forKey: name) + } + + return true + } + + /// Sets whether the LDContext is only intended for flag evaluations and should not be indexed by + /// LaunchDarkly. + /// + /// The default value is false. False means that this LDContext represents an entity such as a + /// user that you want to be able to see on the LaunchDarkly dashboard. + /// + /// Setting anonymous to true excludes this LDContext from the database that is used by the + /// dashboard. It does not exclude it from analytics event data, so it is not the same as + /// making attributes private; all non-private attributes will still be included in events and + /// data export. + /// + /// This value is also addressable in evaluations as the attribute name "anonymous". It is + /// always treated as a boolean true or false in evaluations. + public mutating func anonymous(_ anonymous: Bool) { + self.anonymous = anonymous + } + + /// Provide a reference to designate any number of LDContext attributes as private: that is, + /// their values will not be sent to LaunchDarkly. + /// + /// This action only affects analytics events that involve this particular `LDContext`. To mark some (or all) + /// Context attributes as private for all contexts, use the overall event configuration for the SDK. + /// + /// In this example, firstName is marked as private, but lastName is not: + /// + /// ```swift + /// var builder = LDContextBuilder(key: "my-key") + /// builder.kind("org") + /// builder.trySetValue("firstName", "Pierre") + /// builder.trySetValue("lastName", "Menard") + /// builder.addPrivate(Reference("firstName")) + /// + /// let context = try builder.build().get() + /// ``` + /// + /// The attributes "kind", "key", and "anonymous" cannot be made private. + /// + /// This is a metadata property, rather than an attribute that can be addressed in evaluations: that is, + /// a rule clause that references the attribute name "private" will not use this value, but instead will + /// use whatever value (if any) you have set for that name with `trySetValue(...)`. + /// + /// # Designating an entire attribute as private + /// + /// If the parameter is an attribute name such as "email" that does not start with a '/' character, the + /// entire attribute is private. + /// + /// # Designating a property within a JSON object as private + /// + /// If the parameter starts with a '/' character, it is interpreted as a slash-delimited path to a + /// property within a JSON object. The first path component is an attribute name, and each following + /// component is a property name. + /// + /// For instance, suppose that the attribute "address" had the following JSON object value: + /// {"street": {"line1": "abc", "line2": "def"}, "city": "ghi"} + /// + /// - Calling either addPrivateAttribute(Reference("address")) or addPrivateAddress(Reference("/address")) would + /// cause the entire "address" attribute to be private. + /// - Calling addPrivateAttribute("/address/street") would cause the "street" property to be private, so that + /// only {"city": "ghi"} is included in analytics. + /// - Calling addPrivateAttribute("/address/street/line2") would cause only "line2" within "street" to be private, + /// so that {"street": {"line1": "abc"}, "city": "ghi"} is included in analytics. + /// + /// This syntax deliberately resembles JSON Pointer, but other JSON Pointer features such as array + /// indexing are not supported. + /// + /// 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) + } + + /// 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 } + } + + /// Creates a LDContext from the current LDContextBuilder properties. + /// + /// The LDContext is immutable and will not be affected by any subsequent actions on the + /// LDContextBuilder. + /// + /// It is possible to specify invalid attributes for a LDContextBuilder, such as an empty key. + /// In those situations, this method returns a Result.failure + public func build() -> Result { + guard let kind = Kind(self.kind) else { + return Result.failure(.invalidKind) + } + + if kind.isMulti() { + return Result.failure(.requiresMultiBuilder) + } + + var contextKey = "" + var anonymous = self.anonymous + switch self.key { + case let .key(key): + contextKey = key + case .generateKey: + contextKey = LDContext.defaultKey(kind: kind) + anonymous = true + } + + if !allowEmptyKey && contextKey.isEmpty { + return Result.failure(.emptyKey) + } + + var context = LDContext(canonicalizedKey: canonicalizeKeyForKind(kind: kind, key: contextKey, omitUserKind: true)) + context.kind = kind + context.contexts = [] + context.name = self.name + context.anonymous = anonymous + context.privateAttributes = self.privateAttributes + context.key = contextKey + context.attributes = self.attributes + + return Result.success(context) + } +} + +extension LDContextBuilder: TypeIdentifying { } + +/// Contains method for building a multi-context. +/// +/// Use this type if you need to construct a LDContext that has multiple kind values, each with its +/// own nested LDContext. To define a single-kind context, use `LDContextBuilder` instead. +/// +/// Obtain an instance of LDMultiContextBuilder by calling `LDMultiContextBuilder.init()`; then, call +/// `LDMultiContextBuilder.addContext(_:)` to specify the nested LDContext for each kind. +/// LDMultiContextBuilder setters return a reference the same builder, so they can be chained +/// together. +public struct LDMultiContextBuilder { + private var contexts: [LDContext] = [] + + /// Create a new LDMultiContextBuilder with the provided `key`. + public init() {} + + /// Adds a nested context for a specific kind to a LDMultiContextBuilder. + /// + /// It is invalid to add more than one context with the same Kind. This error is detected when + /// you call `LDMultiContextBuilder.build()`. + public mutating func addContext(_ context: LDContext) { + contexts.append(context) + } + + /// Creates a LDContext from the current properties. + /// + /// The LDContext is immutable and will not be affected by any subsequent actions on the + /// LDMultiContextBuilder. + /// + /// It is possible for a LDMultiContextBuilder to represent an invalid state. In those + /// situations, a Result.failure will be returned. + /// + /// If only one context kind was added to the builder, `build` returns a single-kind context rather + /// than a multi-context. + public func build() -> Result { + if contexts.isEmpty { + return Result.failure(.emptyMultiKind) + } + + if contexts.contains(where: { $0.isMulti() }) { + return Result.failure(.nestedMultiKind) + } + + if contexts.count == 1 { + return Result.success(contexts[0]) + } + + let uniqueKinds = Set(contexts.map { context in context.kind }) + if uniqueKinds.count != contexts.count { + return Result.failure(.duplicateKinds) + } + + let sortedContexts = contexts.sorted { $0.kind < $1.kind } + let canonicalizedKey = sortedContexts.map { context in + return canonicalizeKeyForKind(kind: context.kind, key: context.key ?? "", omitUserKind: false) + }.joined(separator: ":") + + var context = LDContext(canonicalizedKey: canonicalizedKey) + context.kind = .multi + context.contexts = sortedContexts + + return Result.success(context) + } +} + +func canonicalizeKeyForKind(kind: Kind, key: String, omitUserKind: Bool) -> String { + if omitUserKind && kind.isUser() { + return key + } + + let encoding = key.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "" + + return "\(kind):\(encoding)" +} diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift new file mode 100644 index 00000000..7ab095a4 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift @@ -0,0 +1,232 @@ +import Foundation + +/// An enumeration describing the individual failure conditions which may occur when constructing a `Reference`. +public enum ReferenceError: Codable, Equatable, Error { + /// empty means that you tried to create a `Reference` from an empty string, or a string that consisted only of a + /// slash. + /// + /// For details of the attribute reference syntax, see `Reference`. + case empty + + /// doubleSlash means that an attribute reference contained a double slash or trailing slash causing one path + /// component to be empty, such as "/a//b" or "/a/b/". + /// + /// For details of the attribute reference syntax, see `Reference`. + case doubleSlash + + /// invalidEscapeSequence means that an attribute reference contained contained a "~" character that was not + /// followed by "0" or "1". + /// + /// For details of the attribute reference syntax, see `Reference`. + case invalidEscapeSequence + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + switch try container.decode(String.self) { + case "empy": + self = .empty + case "doubleSlash": + self = .doubleSlash + default: + self = .invalidEscapeSequence + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.description) + } +} + +extension ReferenceError: CustomStringConvertible { + public var description: String { + switch self { + case .empty: return "empty" + case .doubleSlash: return "doubleSlash" + case .invalidEscapeSequence: return "invalidEscapeSequence" + } + } +} + +/// Represents an attribute name or path expression identifying a value within a Context. +/// +/// This can be used to retrieve a value with `LDContext.getValue(_:)`, or to identify an attribute or +/// nested value that should be considered private with +/// `LDContextBuilder.addPrivateAttribute(_:)` (the SDK configuration can also have a list of +/// private attribute references). +/// +/// This is represented as a separate type, rather than just a string, so that validation and parsing can +/// be done ahead of time if an attribute reference will be used repeatedly later (such as in flag +/// evaluations). +/// +/// If the string starts with '/', then this is treated as a slash-delimited path reference where the +/// first component is the name of an attribute, and subsequent components are the names of nested JSON +/// object properties. In this syntax, the escape sequences "~0" and "~1" represent '~' and '/' +/// respectively within a path component. +/// +/// If the string does not start with '/', then it is treated as the literal name of an attribute. +/// +/// For instance, if the JSON representation of a context is as follows-- +/// +/// ```json +/// { +/// "kind": "user", +/// "key": "123", +/// "name": "xyz", +/// "address": { +/// "street": "99 Main St.", +/// "city": "Westview" +/// }, +/// "a/b": "ok" +/// } +/// ``` +/// +/// -- then +/// +/// - 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, Equatable, Hashable { + private var error: ReferenceError? + private var rawPath: String + private var components: [String] = [] + + static func unescapePath(_ part: String) -> Result { + if !part.contains("~") { + return Result.success(part) + } + + var output = "" + var index = part.startIndex + + while index < part.endIndex { + if part[index] != "~" { + output.append(part[index]) + index = part.index(after: index) + continue + } + + index = part.index(after: index) + if index == part.endIndex { + return Result.failure(.invalidEscapeSequence) + } + + switch part[index] { + case "0": + output.append("~") + case "1": + output.append("/") + default: + return Result.failure(.invalidEscapeSequence) + } + + index = part.index(after: index) + } + + return Result.success(output) + } + + /// Construct a new Reference. + /// + /// This constructor always returns a Reference that preserves the original string, even if + /// validation fails, so that serializing the Reference to JSON will produce the original + /// string. + public init(_ value: String) { + rawPath = value + + if value.isEmpty || value == "/" { + error = .empty + return + } + + if value.prefix(1) != "/" { + components = [value] + return + } + + var referenceComponents: [String] = [] + let parts = value.components(separatedBy: "/") + for (index, part) in parts.enumerated() { + if index == 0 { + // We can ignore the first match since we know we had a leading slash. + continue + } + + // We must have had a double slash + if part.isEmpty { + error = .doubleSlash + return + } + + let result = Reference.unescapePath(part) + switch result { + case .success(let unescapedPath): + referenceComponents.append(unescapedPath) + case .failure(let err): + error = err + return + } + } + + components = referenceComponents + } + + private init() { + rawPath = "" + components = [] + error = nil + } + + public init(literal value: String) { + if value.isEmpty { + self.init(value) + return + } + + self.init() + let str = value.replacingOccurrences(of: "~", with: "~0").replacingOccurrences(of: "/", with: "~1") + self.rawPath = str + self.components = [value] + self.error = nil + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let reference = try container.decode(String.self) + self = Reference(reference) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.rawPath) + } + + /// Returns whether or not the reference provided is valid. + public func isValid() -> Bool { + return error == nil + } + + /// If the reference is invalid, this method will return an error description; otherwise, it + /// will return an empty string. + public func getError() -> ReferenceError? { + return error + } + + internal func depth() -> Int { + return components.count + } + + /// Returns raw string that was passed into constructor. + public func raw() -> String { + return rawPath + } + + internal func component(_ index: Int) -> String? { + if index >= self.depth() { + return nil + } + + return self.components[index] + } +} diff --git a/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift b/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift index 3df691e7..7b51e86f 100644 --- a/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift +++ b/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift @@ -91,7 +91,6 @@ struct DiagnosticSdk: Encodable { } struct DiagnosticConfig: Codable { - let autoAliasingOptOut: Bool let customBaseURI: Bool let customEventsURI: Bool let customStreamURI: Bool @@ -102,17 +101,15 @@ struct DiagnosticConfig: Codable { let allAttributesPrivate: Bool let pollingIntervalMillis: Int let backgroundPollingIntervalMillis: Int - let inlineUsersInEvents: Bool let useReport: Bool let backgroundPollingDisabled: Bool let evaluationReasonsRequested: Bool - let maxCachedUsers: Int + let maxCachedContexts: Int let mobileKeyCount: Int let diagnosticRecordingIntervalMillis: Int let customHeaders: Bool init(config: LDConfig) { - autoAliasingOptOut = config.autoAliasingOptOut customBaseURI = config.baseUrl != LDConfig.Defaults.baseUrl customEventsURI = config.eventsUrl != LDConfig.Defaults.eventsUrl customStreamURI = config.streamUrl != LDConfig.Defaults.streamUrl @@ -120,15 +117,14 @@ struct DiagnosticConfig: Codable { connectTimeoutMillis = Int(exactly: round(config.connectionTimeout * 1_000)) ?? .max eventsFlushIntervalMillis = Int(exactly: round(config.eventFlushInterval * 1_000)) ?? .max streamingDisabled = config.streamingMode == .polling - allAttributesPrivate = config.allUserAttributesPrivate + allAttributesPrivate = config.allContextAttributesPrivate pollingIntervalMillis = Int(exactly: round(config.flagPollingInterval * 1_000)) ?? .max backgroundPollingIntervalMillis = Int(exactly: round(config.backgroundFlagPollingInterval * 1_000)) ?? .max - inlineUsersInEvents = config.inlineUserInEvents useReport = config.useReport backgroundPollingDisabled = !config.enableBackgroundUpdates evaluationReasonsRequested = config.evaluationReasons // While the SDK treats all negative values as unlimited, for consistency we only send -1 for diagnostics - maxCachedUsers = config.maxCachedUsers >= 0 ? config.maxCachedUsers : -1 + maxCachedContexts = config.maxCachedContexts >= 0 ? config.maxCachedContexts : -1 mobileKeyCount = 1 + (config.getSecondaryMobileKeys().count) diagnosticRecordingIntervalMillis = Int(exactly: round(config.diagnosticRecordingInterval * 1_000)) ?? .max customHeaders = !config.additionalHeaders.isEmpty || config.headerDelegate != nil diff --git a/LaunchDarkly/LaunchDarkly/Models/Event.swift b/LaunchDarkly/LaunchDarkly/Models/Event.swift index a0a76f87..3b4fa17c 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Event.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Event.swift @@ -6,15 +6,15 @@ private protocol SubEvent { class Event: Encodable { enum CodingKeys: String, CodingKey { - case key, previousKey, kind, creationDate, user, userKey, value, defaultValue = "default", variation, version, + case key, previousKey, kind, creationDate, context, contextKeys, value, defaultValue = "default", variation, version, data, startDate, endDate, features, reason, metricValue, contextKind, previousContextKind } enum Kind: String { - case feature, debug, identify, custom, summary, alias + case feature, debug, identify, custom, summary static var allKinds: [Kind] { - [feature, debug, identify, custom, summary, alias] + [feature, debug, identify, custom, summary] } } @@ -24,15 +24,10 @@ class Event: Encodable { self.kind = kind } - struct UserInfoKeys { - static let inlineUserInEvents = CodingUserInfoKey(rawValue: "LD_inlineUserInEvents")! - } - func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(kind.rawValue, forKey: .kind) switch self.kind { - case .alias: try (self as? AliasEvent)?.encode(to: encoder, container: container) case .custom: try (self as? CustomEvent)?.encode(to: encoder, container: container) case .debug, .feature: try (self as? FeatureEvent)?.encode(to: encoder, container: container) case .identify: try (self as? IdentifyEvent)?.encode(to: encoder, container: container) @@ -41,42 +36,16 @@ class Event: Encodable { } } -class AliasEvent: Event, SubEvent { - let key: String - let previousKey: String - let contextKind: String - let previousContextKind: String - let creationDate: Date - - init(key: String, previousKey: String, contextKind: String, previousContextKind: String, creationDate: Date = Date()) { - self.key = key - self.previousKey = previousKey - self.contextKind = contextKind - self.previousContextKind = previousContextKind - self.creationDate = creationDate - super.init(kind: .alias) - } - - fileprivate func encode(to encoder: Encoder, container: KeyedEncodingContainer) throws { - var container = container - try container.encode(key, forKey: .key) - try container.encode(previousKey, forKey: .previousKey) - try container.encode(contextKind, forKey: .contextKind) - try container.encode(previousContextKind, forKey: .previousContextKind) - try container.encode(creationDate, forKey: .creationDate) - } -} - class CustomEvent: Event, SubEvent { let key: String - let user: LDUser + let context: LDContext let data: LDValue let metricValue: Double? let creationDate: Date - init(key: String, user: LDUser, data: LDValue = nil, metricValue: Double? = nil, creationDate: Date = Date()) { + init(key: String, context: LDContext, data: LDValue = nil, metricValue: Double? = nil, creationDate: Date = Date()) { self.key = key - self.user = user + self.context = context self.data = data self.metricValue = metricValue self.creationDate = creationDate @@ -86,14 +55,8 @@ class CustomEvent: Event, SubEvent { fileprivate func encode(to encoder: Encoder, container: KeyedEncodingContainer) throws { var container = container try container.encode(key, forKey: .key) - if encoder.userInfo[Event.UserInfoKeys.inlineUserInEvents] as? Bool ?? false { - try container.encode(user, forKey: .user) - } else { - try container.encode(user.key, forKey: .userKey) - } - if user.isAnonymous == true { - try container.encode("anonymousUser", forKey: .contextKind) - } + try container.encode(context.contextKeys(), forKey: .contextKeys) + if data != .null { try container.encode(data, forKey: .data) } @@ -104,19 +67,19 @@ class CustomEvent: Event, SubEvent { class FeatureEvent: Event, SubEvent { let key: String - let user: LDUser + let context: LDContext let value: LDValue let defaultValue: LDValue let featureFlag: FeatureFlag? let includeReason: Bool let creationDate: Date - init(key: String, user: LDUser, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, includeReason: Bool, isDebug: Bool, creationDate: Date = Date()) { + init(key: String, context: LDContext, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, includeReason: Bool, isDebug: Bool, creationDate: Date = Date()) { self.key = key self.value = value self.defaultValue = defaultValue self.featureFlag = featureFlag - self.user = user + self.context = context self.includeReason = includeReason self.creationDate = creationDate super.init(kind: isDebug ? .debug : .feature) @@ -125,13 +88,10 @@ class FeatureEvent: Event, SubEvent { fileprivate func encode(to encoder: Encoder, container: KeyedEncodingContainer) throws { var container = container try container.encode(key, forKey: .key) - if kind == .debug || encoder.userInfo[Event.UserInfoKeys.inlineUserInEvents] as? Bool ?? false { - try container.encode(user, forKey: .user) + if kind == .debug { + try container.encode(context, forKey: .context) } else { - try container.encode(user.key, forKey: .userKey) - } - if kind == .feature && user.isAnonymous == true { - try container.encode("anonymousUser", forKey: .contextKind) + try container.encode(context.contextKeys(), forKey: .contextKeys) } try container.encodeIfPresent(featureFlag?.variation, forKey: .variation) try container.encodeIfPresent(featureFlag?.versionForEvents, forKey: .version) @@ -145,19 +105,19 @@ class FeatureEvent: Event, SubEvent { } class IdentifyEvent: Event, SubEvent { - let user: LDUser + let context: LDContext let creationDate: Date - init(user: LDUser, creationDate: Date = Date()) { - self.user = user + init(context: LDContext, creationDate: Date = Date()) { + self.context = context self.creationDate = creationDate super.init(kind: .identify) } fileprivate func encode(to encoder: Encoder, container: KeyedEncodingContainer) throws { var container = container - try container.encode(user.key, forKey: .key) - try container.encode(user, forKey: .user) + try container.encode(context.fullyQualifiedKey(), forKey: .key) + try container.encode(context, forKey: .context) try container.encode(creationDate, forKey: .creationDate) } } diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift index 909696c8..8bd8465e 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift @@ -4,13 +4,13 @@ struct FlagRequestTracker { let startDate = Date() var flagCounters: [LDFlagKey: FlagCounter] = [:] - mutating func trackRequest(flagKey: LDFlagKey, reportedValue: LDValue, featureFlag: FeatureFlag?, defaultValue: LDValue) { + mutating func trackRequest(flagKey: LDFlagKey, reportedValue: LDValue, featureFlag: FeatureFlag?, defaultValue: LDValue, context: LDContext) { if flagCounters[flagKey] == nil { flagCounters[flagKey] = FlagCounter() } guard let flagCounter = flagCounters[flagKey] else { return } - flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue) + flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue, context: context) Log.debug(typeName(and: #function) + "\n\tflagKey: \(flagKey)" + "\n\treportedValue: \(reportedValue), " @@ -26,7 +26,7 @@ extension FlagRequestTracker: TypeIdentifying { } final class FlagCounter: Encodable { enum CodingKeys: String, CodingKey { - case defaultValue = "default", counters + case defaultValue = "default", counters, contextKinds } enum CounterCodingKeys: String, CodingKey { @@ -35,8 +35,9 @@ final class FlagCounter: Encodable { private(set) var defaultValue: LDValue = .null private(set) var flagValueCounters: [CounterKey: CounterValue] = [:] + private(set) var contextKinds: Set = Set() - func trackRequest(reportedValue: LDValue, featureFlag: FeatureFlag?, defaultValue: LDValue) { + func trackRequest(reportedValue: LDValue, featureFlag: FeatureFlag?, defaultValue: LDValue, context: LDContext) { self.defaultValue = defaultValue let key = CounterKey(variation: featureFlag?.variation, version: featureFlag?.versionForEvents) if let counter = flagValueCounters[key] { @@ -44,11 +45,16 @@ final class FlagCounter: Encodable { } else { flagValueCounters[key] = CounterValue(value: reportedValue) } + + context.contextKeys().forEach { kind, _ in + contextKinds.insert(kind) + } } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(defaultValue, forKey: .defaultValue) + try container.encode(contextKinds, forKey: .contextKinds) var countersContainer = container.nestedUnkeyedContainer(forKey: .counters) try flagValueCounters.forEach { (key, value) in var counterContainer = countersContainer.nestedContainer(keyedBy: CounterCodingKeys.self) diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift index 4b5b46a9..5ccc9675 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift @@ -5,7 +5,7 @@ import Foundation explanation of how it is calculated. */ public final class LDEvaluationDetail { - /// The value of the flag for the current user. + /// The value of the flag for the current context. public let value: T /// The index of the returned value within the flag's list of variations, or `nil` if the default was returned. public let variationIndex: Int? diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index 2160e84a..bbd40808 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -4,7 +4,7 @@ import Foundation public enum LDStreamingMode { /** In streaming mode, the SDK uses a streaming connection to receive feature flag data from LaunchDarkly. When a flag - is updated in the dashboard, the stream notifies the SDK of changes to the evaluation result for the current user. + is updated in the dashboard, the stream notifies the SDK of changes to the evaluation result for the current context. Streaming mode is not available on watchOS. On iOS and tvOS, the client app must be running in the foreground to use a streaming connection. If streaming mode is not available, the SDK reverts to polling mode. @@ -23,7 +23,7 @@ typealias MobileKey = String /** A callback for dynamically setting http headers when connection & reconnecting to a stream or on every poll request. This function should return a copy of the headers received with - any modifications or additions needed. Removing headers is discouraged as it may cause + any modifications or additions needed. Removing headers is discouraged as it may cause requests to fail. - parameter url: The endpoint that is being connected to @@ -115,7 +115,7 @@ public struct LDConfig { static let eventsUrl = URL(string: "https://mobile.launchdarkly.com")! /// The default base url for connecting to streaming service static let streamUrl = URL(string: "https://clientstream.launchdarkly.com")! - + /// The default maximum number of events the LDClient can store static let eventCapacity = 100 @@ -135,25 +135,22 @@ public struct LDConfig { /// The default mode to set LDClient online on a start call. (true) static let startOnline = true - /// The default setting for private user attributes. (false) - static let allUserAttributesPrivate = false - /// The default private user attribute list (nil) - static let privateUserAttributes: [UserAttribute] = [] + /// The default setting for private context attributes. (false) + static let allContextAttributesPrivate = false + /// The default private context attribute list (nil) + static let privateContextAttributes: [Reference] = [] /// The default HTTP request method for stream connections and feature flag requests. When true, these requests will use the non-standard verb `REPORT`. When false, these requests will use the standard verb `GET`. (false) static let useReport = false - /// The default setting controlling the amount of user data sent in events. When true the SDK will generate events using the full LDUser, excluding private attributes. When false the SDK will generate events using only the LDUser.key. (false) - static let inlineUserInEvents = false - /// The default setting controlling information logged to the console, and modifying some setting ranges to facilitate debugging. (false) static let debugMode = false - + /// The default setting for whether we request evaluation reasons for all flags. (false) static let evaluationReasons = false - /// The default setting for the maximum number of locally cached users. (5) - static let maxCachedUsers = 5 + /// The default setting for the maximum number of locally cached contexts. (5) + static let maxCachedContexts = 5 /// The default setting for whether sending diagnostic data is disabled. (false) static let diagnosticOptOut = false @@ -175,9 +172,6 @@ public struct LDConfig { /// a closure to allow dynamic changes of headers on connect & reconnect static let headerDelegate: RequestHeaderTransform? = nil - - /// should anonymous users automatically be aliased when identifying - static let autoAliasingOptOut: Bool = false } /// Constants relevant to setting up an `LDConfig` @@ -262,30 +256,30 @@ public struct LDConfig { } } private var allowBackgroundUpdates: Bool - + /// Controls LDClient start behavior. When true, calling start causes LDClient to go online. When false, calling start causes LDClient to remain offline. If offline at start, set the client online to receive flag updates. (Default: true) public var startOnline: Bool = Defaults.startOnline /** - Treat all user attributes as private for event reporting for all users. + Treat all context attributes as private for event reporting for all contexts. The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - When true, ignores values in either LDConfig.privateUserAttributes or LDUser.privateAttributes. (Default: false) + When true, ignores values in either LDConfig.privateContextAttributes or LDContext.privateAttributes. (Default: false) - See Also: `privateUserAttributes` and `LDUser.privateAttributes` + See Also: `privateContextAttributes` and `LDContext.privateAttributes` */ - public var allUserAttributesPrivate: Bool = Defaults.allUserAttributesPrivate + public var allContextAttributesPrivate: Bool = Defaults.allContextAttributesPrivate /** - User attributes and top level custom dictionary keys to treat as private for event reporting for all users. + Context attributes and top level custom dictionary keys to treat as private for event reporting for all contexts. The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - To set private user attributes for a specific user, see `LDUser.privateAttributes`. (Default: nil) + To set private context attributes for a specific context, see `LDContext.privateAttributes`. (Default: nil) - See Also: `allUserAttributesPrivate` and `LDUser.privateAttributes`. + See Also: `allContextAttributesPrivate` and `LDContext.privateAttributes`. */ - public var privateUserAttributes: [UserAttribute] = Defaults.privateUserAttributes + public var privateContextAttributes: [Reference] = Defaults.privateContextAttributes /** Directs the SDK to use REPORT for HTTP requests for feature flag data. (Default: `false`) @@ -296,19 +290,14 @@ public struct LDConfig { public var useReport: Bool = Defaults.useReport private static let flagRetryStatusCodes = [HTTPURLResponse.StatusCodes.methodNotAllowed, HTTPURLResponse.StatusCodes.badRequest, HTTPURLResponse.StatusCodes.notImplemented] - /** - Controls how the SDK reports the user in analytics event reports. When set to true, event reports will contain the user attributes, except attributes marked as private. When set to false, event reports will contain the user's key only, reducing the size of event reports. (Default: false) - */ - public var inlineUserInEvents: Bool = Defaults.inlineUserInEvents - /// Enables logging for debugging. (Default: false) public var isDebugMode: Bool = Defaults.debugMode - + /// Enables requesting evaluation reasons for all flags. (Default: false) public var evaluationReasons: Bool = Defaults.evaluationReasons - - /// An Integer that tells UserEnvironmentFlagCache the maximum number of users to locally cache. Can be set to -1 for unlimited cached users. - public var maxCachedUsers: Int = Defaults.maxCachedUsers + + /// An Integer that tells ContextEnvironmentFlagCache the maximum number of contexts to locally cache. Can be set to -1 for unlimited cached contexts. + public var maxCachedContexts: Int = Defaults.maxCachedContexts /** Set to true to opt out of sending diagnostic data. (Default: false) @@ -347,9 +336,6 @@ public struct LDConfig { let environmentReporter: EnvironmentReporting - /// should anonymous users automatically be aliased when identifying - public var autoAliasingOptOut: Bool = Defaults.autoAliasingOptOut - /// A Dictionary of identifying names to unique mobile keys for all environments private var mobileKeys: [String: String] { var internalMobileKeys = getSecondaryMobileKeys() @@ -387,10 +373,10 @@ public struct LDConfig { public func getSecondaryMobileKeys() -> [String: String] { return _secondaryMobileKeys } - + /// Internal variable for secondaryMobileKeys computed property private var _secondaryMobileKeys: [String: String] - + // Internal constructor to enable automated testing init(mobileKey: String, environmentReporter: EnvironmentReporting) { self.mobileKey = mobileKey @@ -440,19 +426,17 @@ extension LDConfig: Equatable { && lhs.streamingMode == rhs.streamingMode && lhs.enableBackgroundUpdates == rhs.enableBackgroundUpdates && lhs.startOnline == rhs.startOnline - && lhs.allUserAttributesPrivate == rhs.allUserAttributesPrivate - && Set(lhs.privateUserAttributes) == Set(rhs.privateUserAttributes) + && lhs.allContextAttributesPrivate == rhs.allContextAttributesPrivate + && Set(lhs.privateContextAttributes) == Set(rhs.privateContextAttributes) && lhs.useReport == rhs.useReport - && lhs.inlineUserInEvents == rhs.inlineUserInEvents && lhs.isDebugMode == rhs.isDebugMode && lhs.evaluationReasons == rhs.evaluationReasons - && lhs.maxCachedUsers == rhs.maxCachedUsers + && lhs.maxCachedContexts == rhs.maxCachedContexts && lhs.diagnosticOptOut == rhs.diagnosticOptOut && lhs.diagnosticRecordingInterval == rhs.diagnosticRecordingInterval && lhs.wrapperName == rhs.wrapperName && lhs.wrapperVersion == rhs.wrapperVersion && lhs.additionalHeaders == rhs.additionalHeaders - && lhs.autoAliasingOptOut == rhs.autoAliasingOptOut } } diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift index 2c16339a..7926b503 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift @@ -3,16 +3,8 @@ import Foundation /** LDUser allows clients to collect information about users in order to refine the feature flag values sent to the SDK. - For example, the client app may launch with the SDK defined anonymous user. As the user works with the client app, - information may be collected as needed and sent to LaunchDarkly. The client app controls the information collected. - Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. - - The SDK caches last known feature flags for use on app startup to provide continuity with the last app run. Provided - the `LDClient` is online and can establish a connection with LaunchDarkly servers, cached information will only be used - a very short time. Once the latest feature flags arrive at the SDK, the SDK no longer uses cached feature flags. The - SDK retains feature flags on the last 5 client defined users. The SDK will retain feature flags until they are - overwritten by a different user's feature flags, or until the user removes the app from the device. The SDK does not - cache user information collected. + The usage of LDUser is no longer recommended and is retained only to ease the adoption of the `LDContext` class. New + code using this SDK should make use of the `LDContextBuilder` to construct an equivalent `Kind.user` kind context. */ public struct LDUser: Encodable, Equatable { @@ -22,8 +14,6 @@ public struct LDUser: Encodable, Equatable { /// Client app defined string that uniquely identifies the user. If the client app does not define a key, the SDK will assign an identifier associated with the anonymous user. The key cannot be made private. public var key: String - /// The secondary key for the user. Read the [documentation](https://docs.launchdarkly.com/home/flags/rollouts) for more information on it's use for percentage rollout bucketing. - public var secondary: String? /// Client app defined name for the user. (Default: nil) public var name: String? /// Client app defined first name for the user. (Default: nil) @@ -40,20 +30,8 @@ public struct LDUser: Encodable, Equatable { public var avatar: String? /// Client app defined dictionary for the user. The client app may declare top level dictionary items as private, see `privateAttributes` for details. (Default: [:]) public var custom: [String: LDValue] - /// Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. isAnonymous cannot be made private. (Default: true) - public var isAnonymous: Bool { - get { isAnonymousNullable == true } - set { isAnonymousNullable = newValue } - } - - /** - Whether or not the user is anonymous, if that has been specified (or set due to the lack of a `key` property). - - Although the `isAnonymous` property defaults to `false` in terms of LaunchDarkly's indexing behavior, for historical - reasons flag evaluation may behave differently if the value is explicitly set to `false` verses being omitted. This - field allows treating the property as optional for consisent evaluation with other LaunchDarkly SDKs. - */ - public var isAnonymousNullable: Bool? + /// Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. isAnonymous cannot be made private. (Default: false) + public var isAnonymous: Bool /** Client app defined privateAttributes for the user. @@ -78,7 +56,6 @@ public struct LDUser: Encodable, Equatable { - parameter custom: Client app defined dictionary for the user. The client app may declare top level dictionary items as private. If the client app defines custom as private, the SDK considers the dictionary private except for device & operatingSystem (which cannot be made private). See `privateAttributes` for details. (Default: nil) - parameter isAnonymous: Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. (Default: nil) - parameter privateAttributes: Client app defined privateAttributes for the user. (Default: nil) - - parameter secondary: Secondary attribute value. (Default: nil) */ public init(key: String? = nil, name: String? = nil, @@ -90,12 +67,10 @@ public struct LDUser: Encodable, Equatable { avatar: String? = nil, custom: [String: LDValue]? = nil, isAnonymous: Bool? = nil, - privateAttributes: [UserAttribute]? = nil, - secondary: String? = nil) { + privateAttributes: [UserAttribute]? = nil) { let environmentReporter = EnvironmentReporter() let selectedKey = key ?? LDUser.defaultKey(environmentReporter: environmentReporter) self.key = selectedKey - self.secondary = secondary self.name = name self.firstName = firstName self.lastName = lastName @@ -103,13 +78,13 @@ public struct LDUser: Encodable, Equatable { self.ipAddress = ipAddress self.email = email self.avatar = avatar - self.isAnonymousNullable = isAnonymous if isAnonymous == nil && selectedKey == LDUser.defaultKey(environmentReporter: environmentReporter) { - self.isAnonymousNullable = true + self.isAnonymous = true + } else { + // If not nil, use the value, otherwise false. + self.isAnonymous = isAnonymous ?? false; } self.custom = custom ?? [:] - self.custom.merge(["device": .string(environmentReporter.deviceModel), - "os": .string(environmentReporter.systemVersion)]) { lhs, _ in lhs } self.privateAttributes = privateAttributes ?? [] Log.debug(typeName(and: #function) + "user: \(self)") } @@ -118,10 +93,7 @@ public struct LDUser: Encodable, Equatable { Internal initializer that accepts an environment reporter, used for testing */ init(environmentReporter: EnvironmentReporting) { - self.init(key: LDUser.defaultKey(environmentReporter: environmentReporter), - custom: ["device": .string(environmentReporter.deviceModel), - "os": .string(environmentReporter.systemVersion)], - isAnonymous: true) + self.init(key: LDUser.defaultKey(environmentReporter: environmentReporter), isAnonymous: true) } private func value(for attribute: UserAttribute) -> Any? { @@ -137,6 +109,52 @@ public struct LDUser: Encodable, Equatable { static let globalPrivateAttributes = CodingUserInfoKey(rawValue: "LD_globalPrivateAttributes")! } + /** + Internal helper method to convert an LDUser to an LDContext. + + Ideally we would do this as the LDUser was being built. However, the LDUser properties are publicly accessible, which makes that approach problematic. + */ + internal func toContext() -> Result { + var contextBuilder = LDContextBuilder(key: key) + + // Custom attributes must be processed first in case built-in attributes + // need to override those values + custom.forEach { (key, value) in + contextBuilder.trySetValue(key, value) + } + + if let name = name { + contextBuilder.name(name) + } + + contextBuilder.anonymous(isAnonymous) + + if let firstName = firstName { + contextBuilder.trySetValue("firstName", firstName.toLDValue()) + } + if let lastName = lastName { + contextBuilder.trySetValue("lastName", lastName.toLDValue()) + } + if let country = country { + contextBuilder.trySetValue("country", country.toLDValue()) + } + if let ipAddress = ipAddress { + contextBuilder.trySetValue("ipAddress", ipAddress.toLDValue()) + } + if let email = email { + contextBuilder.trySetValue("email", email.toLDValue()) + } + if let avatar = avatar { + contextBuilder.trySetValue("avatar", avatar.toLDValue()) + } + + privateAttributes.forEach { privateAttribute in + contextBuilder.addPrivateAttribute(Reference(literal: privateAttribute.name)) + } + + return contextBuilder.build() + } + public func encode(to encoder: Encoder) throws { let includePrivateAttributes = encoder.userInfo[UserInfoKeys.includePrivateAttributes] as? Bool ?? false let allAttributesPrivate = encoder.userInfo[UserInfoKeys.allAttributesPrivate] as? Bool ?? false @@ -150,8 +168,8 @@ public struct LDUser: Encodable, Equatable { var container = encoder.container(keyedBy: DynamicKey.self) try container.encode(key, forKey: DynamicKey(stringValue: "key")!) - if let anonymous = isAnonymousNullable { - try container.encode(anonymous, forKey: DynamicKey(stringValue: "anonymous")!) + if isAnonymous { + try container.encode(true, forKey: DynamicKey(stringValue: "anonymous")!) } try LDUser.optionalAttributes.forEach { attribute in diff --git a/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift b/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift index 069b45bc..d3095972 100644 --- a/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift +++ b/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift @@ -17,8 +17,6 @@ public class UserAttribute: Equatable, Hashable { public struct BuiltIn { /// Represents the user key attribute. public static let key = UserAttribute("key") { $0.key } - /// Represents the secondary key attribute. - public static let secondaryKey = UserAttribute("secondary") { $0.secondary } /// Represents the IP address attribute. public static let ip = UserAttribute("ip") { $0.ipAddress } // swiftlint:disable:this identifier_name /// Represents the email address attribute. @@ -36,7 +34,7 @@ public class UserAttribute: Equatable, Hashable { /// Represents the anonymous attribute. public static let anonymous = UserAttribute("anonymous") { $0.isAnonymous } - static let allBuiltIns = [key, secondaryKey, ip, email, name, avatar, firstName, lastName, country, anonymous] + static let allBuiltIns = [key, ip, email, name, avatar, firstName, lastName, country, anonymous] } static var builtInMap = { return BuiltIn.allBuiltIns.reduce(into: [:]) { $0[$1.name] = $1 } }() diff --git a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 7cc30e85..2f9cfb39 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift @@ -14,7 +14,7 @@ extension EventSource: DarklyStreamingProvider {} protocol DarklyServiceProvider: AnyObject { var config: LDConfig { get } - var user: LDUser { get set } + var context: LDContext { get set } var diagnosticCache: DiagnosticCaching? { get } func getFeatureFlags(useReport: Bool, completion: ServiceCompletionHandler?) @@ -32,8 +32,8 @@ final class DarklyService: DarklyServiceProvider { } struct FlagRequestPath { - static let get = "msdk/evalx/users" - static let report = "msdk/evalx/user" + static let get = "msdk/evalx/contexts" + static let report = "msdk/evalx/context" } struct StreamRequestPath { @@ -46,16 +46,16 @@ final class DarklyService: DarklyServiceProvider { } let config: LDConfig - var user: LDUser + var context: LDContext let httpHeaders: HTTPHeaders let diagnosticCache: DiagnosticCaching? private (set) var serviceFactory: ClientServiceCreating private var session: URLSession var flagRequestEtag: String? - init(config: LDConfig, user: LDUser, serviceFactory: ClientServiceCreating) { + init(config: LDConfig, context: LDContext, serviceFactory: ClientServiceCreating) { self.config = config - self.user = user + self.context = context self.serviceFactory = serviceFactory if !config.mobileKey.isEmpty && !config.diagnosticOptOut { @@ -82,8 +82,8 @@ final class DarklyService: DarklyServiceProvider { func getFeatureFlags(useReport: Bool, completion: ServiceCompletionHandler?) { guard hasMobileKey(#function) else { return } let encoder = JSONEncoder() - encoder.userInfo[LDUser.UserInfoKeys.includePrivateAttributes] = true - guard let userJsonData = try? encoder.encode(user) + encoder.userInfo[LDContext.UserInfoKeys.includePrivateAttributes] = true + guard let contextJsonData = try? encoder.encode(context) else { Log.debug(typeName(and: #function, appending: ": ") + "Aborting. Unable to create flagRequest.") return @@ -93,12 +93,12 @@ final class DarklyService: DarklyServiceProvider { if let etag = flagRequestEtag { headers.merge([HTTPHeaders.HeaderKey.ifNoneMatch: etag]) { orig, _ in orig } } - var request = URLRequest(url: flagRequestUrl(useReport: useReport, getData: userJsonData), + var request = URLRequest(url: flagRequestUrl(useReport: useReport, getData: contextJsonData), ldHeaders: headers, ldConfig: config) if useReport { request.httpMethod = URLRequest.HTTPMethods.report - request.httpBody = userJsonData + request.httpBody = contextJsonData } self.session.dataTask(with: request) { [weak self] data, response, error in @@ -144,12 +144,12 @@ final class DarklyService: DarklyServiceProvider { // MARK: Streaming - func createEventSource(useReport: Bool, - handler: EventHandler, + func createEventSource(useReport: Bool, + handler: EventHandler, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider { let encoder = JSONEncoder() - encoder.userInfo[LDUser.UserInfoKeys.includePrivateAttributes] = true - let userJsonData = try? encoder.encode(user) + encoder.userInfo[LDContext.UserInfoKeys.includePrivateAttributes] = true + let contextJsonData = try? encoder.encode(context) var streamRequestUrl = config.streamUrl.appendingPathComponent(StreamRequestPath.meval) var connectMethod = HTTPRequestMethod.get @@ -157,9 +157,9 @@ final class DarklyService: DarklyServiceProvider { if useReport { connectMethod = HTTPRequestMethod.report - connectBody = userJsonData + connectBody = contextJsonData } else { - streamRequestUrl.appendPathComponent(userJsonData?.base64UrlEncodedString ?? "", isDirectory: false) + streamRequestUrl.appendPathComponent(contextJsonData?.base64UrlEncodedString ?? "", isDirectory: false) } return serviceFactory.makeStreamingProvider(url: shouldGetReasons(url: streamRequestUrl), diff --git a/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift b/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift index a8bdd949..abbd3a9a 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift @@ -17,7 +17,7 @@ struct HTTPHeaders { struct HeaderValue { static let apiKey = "api_key" static let applicationJson = "application/json" - static let eventSchema3 = "3" + static let eventSchema4 = "4" } private let mobileKey: String @@ -67,7 +67,7 @@ struct HTTPHeaders { var headers = baseHeaders headers[HeaderKey.contentType] = HeaderValue.applicationJson headers[HeaderKey.accept] = HeaderValue.applicationJson - headers[HeaderKey.eventSchema] = HeaderValue.eventSchema3 + headers[HeaderKey.eventSchema] = HeaderValue.eventSchema4 return withAdditionalHeaders(headers) } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index b9dfcfc9..f1db29b0 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -1,16 +1,16 @@ import Foundation /** - The LDClient is the heart of the SDK, providing client apps running iOS, watchOS, macOS, or tvOS access to LaunchDarkly services. This singleton provides the ability to set a configuration (LDConfig) that controls how the LDClient talks to LaunchDarkly servers, and a user (LDUser) that provides finer control on the feature flag values delivered to LDClient. Once the LDClient has started, it connects to LaunchDarkly's servers to get the feature flag values you set in the Dashboard. + The LDClient is the heart of the SDK, providing client apps running iOS, watchOS, macOS, or tvOS access to LaunchDarkly services. This singleton provides the ability to set a configuration (LDConfig) that controls how the LDClient talks to LaunchDarkly servers, and a context (LDContext) that provides finer control on the feature flag values delivered to LDClient. Once the LDClient has started, it connects to LaunchDarkly's servers to get the feature flag values you set in the Dashboard. ### Objc Classes - The SDK creates an Objective-C native style API by wrapping Swift specific classes, properties, and methods into Objective-C wrapper classes prefixed by `Objc`. By defining Objective-C specific names, client apps written in Objective-C can use a native coding style, including using familiar LaunchDarkly SDK names like `LDClient`, `LDConfig`, and `LDUser`. Objective-C developers should refer to the Objc documentation by following the Objc specific links following type, property, and method names. + The SDK creates an Objective-C native style API by wrapping Swift specific classes, properties, and methods into Objective-C wrapper classes prefixed by `Objc`. By defining Objective-C specific names, client apps written in Objective-C can use a native coding style, including using familiar LaunchDarkly SDK names like `LDClient`, `LDConfig`, and `LDContext`. Objective-C developers should refer to the Objc documentation by following the Objc specific links following type, property, and method names. ## Usage ### Startup - 1. To customize, configure a LDConfig (`ObjcLDConfig`) and LDUser (`ObjcLDUser`). Both give you additional control over the feature flags delivered to the LDClient. See `ObjcLDConfig` & `ObjcLDUser` for more details. + 1. To customize, configure a LDConfig (`ObjcLDConfig`) and LDContext (`ObjcLDContxt`). Both give you additional control over the feature flags delivered to the LDClient. See `ObjcLDConfig` & `ObjcLDContext` for more details. - The mobileKey set into the `LDConfig` comes from your LaunchDarkly Account settings (on the left, at the bottom). If you have multiple projects be sure to choose the correct Mobile key. - 2. Call `[ObjcLDClient startWithConfig: user: completion:]` (`ObjcLDClient.startWithConfig(_:config:user:completion:)`) - - If you do not pass in a LDUser, LDCLient will create a default for you. + 2. Call `[ObjcLDClient startWithConfig: context: completion:]` (`ObjcLDClient.startWithConfig(_:config:context:completion:)`) + - If you do not pass in a LDContext, LDCLient will create a default for you. - The optional completion closure allows the LDClient to notify your app when it has gone online. 3. Because the LDClient is a singleton, you do not have to keep a reference to it in your code. @@ -46,7 +46,7 @@ public final class ObjcLDClient: NSObject { // MARK: - State Controls and Indicators private var ldClient: LDClient - + /** Reports the online/offline state of the LDClient. @@ -79,7 +79,7 @@ public final class ObjcLDClient: NSObject { When offline, the SDK does not attempt to communicate with LaunchDarkly servers. Client apps can request feature flag values and set/change feature flag observers while offline. The SDK will collect events while offline. - The SDK protects itself from multiple rapid calls to `setOnline:YES` by enforcing an increasing delay (called *throttling*) each time `setOnline:YES` is called within a short time. The first time, the call proceeds normally. For each subsequent call the delay is enforced, and if waiting, increased to a maximum delay. When the delay has elapsed, the `setOnline:YES` will proceed, assuming that the client app has not called `setOnline:NO` during the delay. Therefore a call to `setOnline:YES` may not immediately result in the LDClient going online. Client app developers should consider this situation abnormal, and take steps to prevent the client app from making multiple rapid `setOnline:YES` calls. Calls to `setOnline:NO` are not throttled. Note that calls to `start(config: user: completion:)`, and setting the `config` or `user` can also call `setOnline:YES` under certain conditions. After the delay, the SDK resets and the client app can make a susequent call to `setOnline:YES` without being throttled. + The SDK protects itself from multiple rapid calls to `setOnline:YES` by enforcing an increasing delay (called *throttling*) each time `setOnline:YES` is called within a short time. The first time, the call proceeds normally. For each subsequent call the delay is enforced, and if waiting, increased to a maximum delay. When the delay has elapsed, the `setOnline:YES` will proceed, assuming that the client app has not called `setOnline:NO` during the delay. Therefore a call to `setOnline:YES` may not immediately result in the LDClient going online. Client app developers should consider this situation abnormal, and take steps to prevent the client app from making multiple rapid `setOnline:YES` calls. Calls to `setOnline:NO` are not throttled. Note that calls to `start(config: context: completion:)`, and setting the `config` or `context` can also call `setOnline:YES` under certain conditions. After the delay, the SDK resets and the client app can make a susequent call to `setOnline:YES` without being throttled. Client apps can set a completion block called when the setOnline call completes. For unthrottled `setOnline:YES` and all `setOnline:NO` calls, the SDK will call the block immediately on completion of this method. For throttled `setOnline:YES` calls, the SDK will call the block after the throttling delay at the completion of the setOnline method. @@ -88,7 +88,7 @@ public final class ObjcLDClient: NSObject { - parameter goOnline: Desired online/offline mode for the LDClient - parameter completion: Completion block called when setOnline completes. (Optional) */ - @objc public func setOnline(_ goOnline: Bool, completion:(() -> Void)? = nil) { + @objc public func setOnline(_ goOnline: Bool, completion: (() -> Void)? = nil) { ldClient.setOnline(goOnline, completion: completion) } @@ -104,28 +104,46 @@ public final class ObjcLDClient: NSObject { } /** - The LDUser set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the user. See `LDUser` for details about what information can be retained. + The LDContext set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the context. See `LDContext` for details about what information can be retained. - The client app can change the current LDUser by calling this method. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. When a new user is set, the LDClient goes offline and sets the new user. If the client was online when the new user was set, it goes online again, subject to a throttling delay if in force (see `setOnline(_: completion:)` for details). + The client app can change the current LDContext by calling this method. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. When a new context is set, the LDClient goes offline and sets the new context. If the client was online when the new context was set, it goes online again, subject to a throttling delay if in force (see `setOnline(_: completion:)` for details). - - parameter user: The ObjcLDUser set with the desired user. + - parameter context: The ObjcLDContext set with the desired context. */ + @objc public func identify(context: ObjcLDContext) { + ldClient.identify(context: context.context, completion: nil) + } + + /** + Deprecated identify method which accepts a legacy LDUser instead of an LDContext. + + This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `identify(context)` for details. + */ @objc public func identify(user: ObjcLDUser) { ldClient.identify(user: user.user, completion: nil) } /** - The LDUser set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the user. See `LDUser` for details about what information can be retained. + The LDContext set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the context. See `LDContext` for details about what information can be retained. - Normally, the client app should create and set the LDUser and pass that into `start(config: user: completion:)`. + Normally, the client app should create and set the LDContext and pass that into `start(config: context: completion:)`. - The client app can change the active `user` by calling identify with a new or updated LDUser. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. If the client app does not create a LDUser, LDClient creates an anonymous default user, which can affect the feature flags delivered to the LDClient. + The client app can change the active `context` by calling identify with a new or updated LDContext. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. If the client app does not create a LDContext, LDClient creates an anonymous default context, which can affect the feature flags delivered to the LDClient. - When a new user is set, the LDClient goes offline and sets the new user. If the client was online when the new user was set, it goes online again, subject to a throttling delay if in force (see `setOnline(_: completion:)` for details). To change both the `config` and `user`, set the LDClient offline, set both properties, then set the LDClient online. A completion may be passed to the identify method to allow a client app to know when fresh flag values for the new user are ready. + When a new context is set, the LDClient goes offline and sets the new context. If the client was online when the new context was set, it goes online again, subject to a throttling delay if in force (see `setOnline(_: completion:)` for details). To change both the `config` and `context`, set the LDClient offline, set both properties, then set the LDClient online. A completion may be passed to the identify method to allow a client app to know when fresh flag values for the new context are ready. - - parameter user: The ObjcLDUser set with the desired user. + - parameter context: The ObjcLDContext set with the desired context. - parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays. (Optional) */ + @objc public func identify(context: ObjcLDContext, completion: (() -> Void)? = nil) { + ldClient.identify(context: context.context, completion: completion) + } + + /** + Deprecated identify method which accepts a legacy LDUser instead of an LDContext. + + This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `identify(context:completion:)` for details. + */ @objc public func identify(user: ObjcLDUser, completion: (() -> Void)? = nil) { ldClient.identify(user: user.user, completion: completion) } @@ -221,13 +239,13 @@ public final class ObjcLDClient: NSObject { @objc public func integerVariation(forKey key: LDFlagKey, defaultValue: Int) -> Int { ldClient.intVariation(forKey: key, defaultValue: defaultValue) } - + /** See [integerVariation](x-source-tag://integerVariation) for more information on variation methods. - + - parameter key: The LDFlagKey for the requested feature flag. - parameter defaultValue: The default value to return if the feature flag key does not exist. - + - returns: ObjcLDIntegerEvaluationDetail containing your value as well as useful information on why that value was returned. */ @objc public func integerVariationDetail(forKey key: LDFlagKey, defaultValue: Int) -> ObjcLDIntegerEvaluationDetail { @@ -258,13 +276,13 @@ public final class ObjcLDClient: NSObject { @objc public func doubleVariation(forKey key: LDFlagKey, defaultValue: Double) -> Double { ldClient.doubleVariation(forKey: key, defaultValue: defaultValue) } - + /** See [doubleVariation](x-source-tag://doubleVariation) for more information on variation methods. - + - parameter key: The LDFlagKey for the requested feature flag. - parameter defaultValue: The default value to return if the feature flag key does not exist. - + - returns: ObjcLDDoubleEvaluationDetail containing your value as well as useful information on why that value was returned. */ @objc public func doubleVariationDetail(forKey key: LDFlagKey, defaultValue: Double) -> ObjcLDDoubleEvaluationDetail { @@ -295,13 +313,13 @@ public final class ObjcLDClient: NSObject { @objc public func stringVariation(forKey key: LDFlagKey, defaultValue: String) -> String { ldClient.stringVariation(forKey: key, defaultValue: defaultValue) } - + /** See [stringVariation](x-source-tag://stringVariation) for more information on variation methods. - + - parameter key: The LDFlagKey for the requested feature flag. - parameter defaultValue: The default value to return if the feature flag key does not exist. - + - returns: ObjcLDStringEvaluationDetail containing your value as well as useful information on why that value was returned. */ @objc public func stringVariationDetail(forKey key: LDFlagKey, defaultValue: String) -> ObjcLDStringEvaluationDetail { @@ -330,13 +348,13 @@ public final class ObjcLDClient: NSObject { @objc public func jsonVariation(forKey key: LDFlagKey, defaultValue: ObjcLDValue) -> ObjcLDValue { ObjcLDValue(wrappedValue: ldClient.jsonVariation(forKey: key, defaultValue: defaultValue.wrappedValue)) } - + /** See [arrayVariation](x-source-tag://arrayVariation) for more information on variation methods. - + - parameter key: The LDFlagKey for the requested feature flag. - parameter defaultValue: The default value to return if the feature flag key does not exist. - + - returns: ObjcLDJSONEvaluationDetail containing your value as well as useful information on why that value was returned. */ @objc public func jsonVariationDetail(forKey key: LDFlagKey, defaultValue: ObjcLDValue) -> ObjcLDJSONEvaluationDetail { @@ -539,49 +557,50 @@ public final class ObjcLDClient: NSObject { ldClient.flush() } - /** - Tells the SDK to generate an alias event. - - Associates two users for analytics purposes. - - This can be helpful in the situation where a person is represented by multiple - LaunchDarkly users. This may happen, for example, when a person initially logs into - an application-- the person might be represented by an anonymous user prior to logging - in and a different user after logging in, as denoted by a different user key. - - - parameter context: the user that will be aliased to - - parameter previousContext: the user that will be bound to the new context - */ - @objc public func alias(context: ObjcLDUser, previousContext: ObjcLDUser) { - ldClient.alias(context: context.user, previousContext: previousContext.user) - } - /** - Starts the LDClient using the passed in `config` & `user`. Call this before requesting feature flag values. The LDClient will not go online until you call this method. - Starting the LDClient means setting the `config` & `user`, setting the client online if `config.startOnline` is true (the default setting), and starting event recording. The client app must start the LDClient before it will report feature flag values. If a client does not call `start`, no methods will work. - If the `start` call omits the `user`, the LDClient uses the default `user` if it was never set. + Starts the LDClient using the passed in `config` & `context`. Call this before requesting feature flag values. The LDClient will not go online until you call this method. + Starting the LDClient means setting the `config` & `context`, setting the client online if `config.startOnline` is true (the default setting), and starting event recording. The client app must start the LDClient before it will report feature flag values. If a client does not call `start`, no methods will work. + If the `start` call omits the `context`, the LDClient uses the default `context` if it was never set. If the` start` call includes the optional `completion` closure, LDClient calls the `completion` closure when `setOnline(_: completion:)` embedded in the `init` method completes. This method listens for flag updates so the completion will only return once an update has occurred. The `start` call is subject to throttling delays, therefore the `completion` closure call may be delayed. - Subsequent calls to this method cause the LDClient to return. Normally there should only be one call to start. To change `user`, use `identify`. + Subsequent calls to this method cause the LDClient to return. Normally there should only be one call to start. To change `context`, use `identify`. - parameter configuration: The LDConfig that contains the desired configuration. (Required) - - parameter user: The LDUser set with the desired user. If omitted, LDClient sets a default user. (Optional) + - parameter context: The LDContext set with the desired context. If omitted, LDClient sets a default context. (Optional) - parameter completion: Closure called when the embedded `setOnline` call completes. (Optional) */ /// - Tag: start + @objc public static func start(configuration: ObjcLDConfig, context: ObjcLDContext, completion: (() -> Void)? = nil) { + LDClient.start(config: configuration.config, context: context.context, completion: completion) + } + + /** + Deprecated start method which accepts a legacy LDUser instead of an LDContext. + + This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `start(configuration:context:completion:)` for details. + */ @objc public static func start(configuration: ObjcLDConfig, user: ObjcLDUser, completion: (() -> Void)? = nil) { - LDClient.start(config: configuration.config, user: user.user, completion: completion) - } + LDClient.start(config: configuration.config, user: user.user, completion: completion) + } /** See [start](x-source-tag://start) for more information on starting the SDK. - parameter configuration: The LDConfig that contains the desired configuration. (Required) - - parameter user: The LDUser set with the desired user. If omitted, LDClient sets a default user.. (Optional) + - parameter context: The LDContext set with the desired context. If omitted, LDClient sets a default context.. (Optional) - parameter startWaitSeconds: A TimeInterval that determines when the completion will return if no flags have been returned from the network. - parameter completion: Closure called when the embedded `setOnline` call completes. Takes a Bool that indicates whether the completion timedout as a parameter. (Optional) */ - @objc public static func start(configuration: ObjcLDConfig, user: ObjcLDUser, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { - LDClient.start(config: configuration.config, user: user.user, startWaitSeconds: startWaitSeconds, completion: completion) - } + @objc public static func start(configuration: ObjcLDConfig, context: ObjcLDContext, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { + LDClient.start(config: configuration.config, context: context.context, startWaitSeconds: startWaitSeconds, completion: completion) + } + + /** + Deprecated start method which accepts a legacy LDUser instead of an LDContext. + + This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `start(configuration:context:startWaitSeconds:completion:)` for details. + */ + @objc public static func start(configuration: ObjcLDConfig, user: ObjcLDUser, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { + LDClient.start(config: configuration.config, user: user.user, startWaitSeconds: startWaitSeconds, completion: completion) + } private init(client: LDClient) { ldClient = client diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift index 4b330d81..7966535e 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift @@ -8,7 +8,7 @@ import Foundation @objc(LDConfig) public final class ObjcLDConfig: NSObject { var config: LDConfig - + /// The Mobile key from your [LaunchDarkly Account](app.launchdarkly.com) settings (on the left at the bottom). If you have multiple projects be sure to choose the correct Mobile key. @objc public var mobileKey: String { get { config.mobileKey } @@ -89,30 +89,30 @@ public final class ObjcLDConfig: NSObject { } /** - Treat all user attributes as private for event reporting for all users. + Treat all context attributes as private for event reporting for all contexts. The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - When YES, ignores values in either LDConfig.privateUserAttributes or LDUser.privateAttributes. (Default: NO) + When YES, ignores values in either LDConfig.privateContextAttributes or LDContext.privateAttributes. (Default: NO) - See Also: `privateUserAttributes` and `LDUser.privateAttributes` (`ObjcLDUser.privateAttributes`) + See Also: `privateContextAttributes` and `LDContext.privateAttributes` (`ObjcLDContext.privateAttributes`) */ - @objc public var allUserAttributesPrivate: Bool { - get { config.allUserAttributesPrivate } - set { config.allUserAttributesPrivate = newValue } + @objc public var allContextAttributesPrivate: Bool { + get { config.allContextAttributesPrivate } + set { config.allContextAttributesPrivate = newValue } } /** - User attributes and top level custom dictionary keys to treat as private for event reporting for all users. + Context attributes and top level custom dictionary keys to treat as private for event reporting for all contexts. The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - To set private user attributes for a specific user, see `LDUser.privateAttributes` (`ObjcLDUser.privateAttributes`). (Default: `[]`) + To set private context attributes for a specific context, see `LDContext.privateAttributes` (`ObjcLDContext.privateAttributes`). (Default: `[]`) - See Also: `allUserAttributesPrivate` and `LDUser.privateAttributes` (`ObjcLDUser.privateAttributes`). + See Also: `allContextAttributesPrivate` and `LDContext.privateAttributes` (`ObjcLDContext.privateAttributes`). */ - @objc public var privateUserAttributes: [String] { - get { config.privateUserAttributes.map { $0.name } } - set { config.privateUserAttributes = newValue.map { UserAttribute.forName($0) } } + @objc public var privateContextAttributes: [String] { + get { config.privateContextAttributes.map { $0.raw() } } + set { config.privateContextAttributes = newValue.map { Reference($0) } } } /** @@ -123,14 +123,6 @@ public final class ObjcLDConfig: NSObject { set { config.useReport = newValue } } - /** - Controls how the SDK reports the user in analytics event reports. When set to YES, event reports will contain the user attributes, except attributes marked as private. When set to NO, event reports will contain the user's key only, reducing the size of event reports. (Default: NO) - */ - @objc public var inlineUserInEvents: Bool { - get { config.inlineUserInEvents } - set { config.inlineUserInEvents = newValue } - } - /// Enables logging for debugging. (Default: NO) @objc public var debugMode: Bool { get { config.isDebugMode } @@ -143,10 +135,10 @@ public final class ObjcLDConfig: NSObject { set { config.evaluationReasons = newValue } } - /// An Integer that tells UserEnvironmentFlagCache the maximum number of users to locally cache. Can be set to -1 for unlimited cached users. (Default: 5) - @objc public var maxCachedUsers: Int { - get { config.maxCachedUsers } - set { config.maxCachedUsers = newValue } + /// An Integer that tells ContextEnvironmentFlagCache the maximum number of contexts to locally cache. Can be set to -1 for unlimited cached contexts. (Default: 5) + @objc public var maxCachedContexts: Int { + get { config.maxCachedContexts } + set { config.maxCachedContexts = newValue } } /** @@ -194,7 +186,7 @@ public final class ObjcLDConfig: NSObject { @objc public func setSecondaryMobileKeys(_ keys: [String: String]) throws { try config.setSecondaryMobileKeys(keys) } - + /// LDConfig constructor. Configurable values are all set to their default values. The client app can modify these values as desired. Note that client app developers may prefer to get the LDConfig from `LDClient.config` (`ObjcLDClient.config`) in order to retain previously set values. @objc public init(mobileKey: String) { config = LDConfig(mobileKey: mobileKey) diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift new file mode 100644 index 00000000..8742407c --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift @@ -0,0 +1,304 @@ +import Foundation + +/// LDContext is a collection of attributes that can be referenced in flag evaluations and analytics +/// events. +/// +/// To create an LDContext of a single kind, such as a user, you may use `LDContextBuilder`. +/// +/// To create an LDContext with multiple kinds, use `LDMultiContextBuilder`. +@objc(LDContext) +public final class ObjcLDContext: NSObject { + var context: LDContext + + init(_ context: LDContext) { + self.context = context + } + + /// FullyQualifiedKey returns a string that describes the entire Context based on Kind and Key values. + /// + /// This value is used whenever LaunchDarkly needs a string identifier based on all of the Kind and + /// Key values in the context; the SDK may use this for caching previously seen contexts, for instance. + @objc public func fullyQualifiedKey() -> String { context.fullyQualifiedKey() } + /// - Returns: true if the `LDContext` is a multi-context; false otherwise. + @objc public func isMulti() -> Bool { context.isMulti() } + //// - Returns: A hash mapping a context's kind to its key. + @objc public func contextKeys() -> [String: String] { context.contextKeys() } + /// Looks up the value of any attribute of the `LDContext`, or a value contained within an + /// attribute, based on a `Reference`. This includes only attributes that are addressable in evaluations. + /// + /// This implements the same behavior that the SDK uses to resolve attribute references during a flag + /// evaluation. In a context, the `Reference` can represent a simple attribute name-- either a + /// built-in one like "name" or "key", or a custom attribute that was set by `LDContextBuilder.trySetValue(...)`-- + /// or, it can be a slash-delimited path using a JSON-Pointer-like syntax. See `Reference` for more details. + /// + /// For a multi-context, the only supported attribute name is "kind". + /// + /// If the value is found, the return value is the attribute value, using the type `LDValue` to + /// represent a value of any JSON type. + /// + /// If there is no such attribute, or if the `Reference` is invalid, the return value is nil. + @objc public func getValue(reference: ObjcLDReference) -> ObjcLDValue? { + guard let value = context.getValue(reference.reference) + else { return nil } + + return ObjcLDValue(wrappedValue: value) + } +} + +/// Contains methods for building a single kind `LDContext` with a specified key, defaulting to kind +/// "user". +/// +/// You may use these methods to set additional attributes and/or change the kind before calling +/// `LDContextBuilder.build()`. If you do not change any values, the defaults for the `LDContext` are that its +/// kind is "user", its key is set to whatever value you passed to `LDContextBuilder.init(key:)`, its anonymous attribute +/// is false, and it has no values for any other attributes. +/// +/// To define a multi-context, see `LDMultiContextBuilder`. +@objc(LDContextBuilder) +public final class ObjcLDContextBuilder: NSObject { + var builder: LDContextBuilder + + /// Create a new LDContextBuilder. + /// + /// By default, this builder will create an anonymous LDContext with a generated key. This key will be cached + /// locally and reused for the same context kind. + /// + /// If `LDContextBuilder.key` is called, a key will no longer be generated and the anonymous status will match the + /// value provided by `LDContextBuilder.anonymous` or false by default. + @objc public override init() { + builder = LDContextBuilder() + } + + /// Create a new LDContextBuilder with the provided `key`. + @objc public init(key: String) { + builder = LDContextBuilder(key: key) + } + + // Initializer to wrap the Swift LDContextBuilder into ObjcLDContextBuilder for use in + // Objective-C apps. + init(_ builder: LDContextBuilder) { + self.builder = builder + } + + /// Sets the LDContext's kind attribute. + /// + /// Every LDContext has a kind. Setting it to an empty string is equivalent to the default kind + /// of "user". This value is case-sensitive. Validation rules are as follows: + /// + /// - It may only contain letters, numbers, and the characters ".", "_", and "-". + /// - It cannot equal the literal string "kind". + /// - It cannot equal "multi". + /// + /// If the value is invalid, you will receive an error when `LDContextBuilder.build()` is called. + @objc public func kind(kind: String) { builder.kind(kind) } + /// Sets the LDContext's key attribute. + /// + /// Every LDContext has a key, which is always a string. There are no restrictions on its value other than it cannot + /// be empty. + /// + /// The key attribute can be referenced by flag rules, flag target lists, and segments. + @objc public func key(key: String) { builder.key(key) } + /// Sets the LDContext's name attribute. + /// + /// This attribute is optional. It has the following special rules: + /// + /// - Unlike most other attributes, it is always a string if it is specified. + /// - The LaunchDarkly dashboard treats this attribute as the preferred display name for users. + @objc public func name(name: String) { builder.name(name) } + /// Sets whether the LDContext is only intended for flag evaluations and should not be indexed by + /// LaunchDarkly. + /// + /// The default value is false. False means that this LDContext represents an entity such as a + /// user that you want to be able to see on the LaunchDarkly dashboard. + /// + /// Setting anonymous to true excludes this LDContext from the database that is used by the + /// dashboard. It does not exclude it from analytics event data, so it is not the same as + /// making attributes private; all non-private attributes will still be included in events and + /// data export. + /// + /// This value is also addressable in evaluations as the attribute name "anonymous". It is + /// always treated as a boolean true or false in evaluations. + @objc public func anonymous(anonymous: Bool) { builder.anonymous(anonymous) } + /// Provide a reference to designate any number of LDContext attributes as private: that is, + /// their values will not be sent to LaunchDarkly. + /// + /// This action only affects analytics events that involve this particular `LDContext`. To mark some (or all) + /// Context attributes as private for all contexts, use the overall event configuration for the SDK. + /// + /// In this example, firstName is marked as private, but lastName is not: + /// + /// ```swift + /// var builder = LDContextBuilder(key: "my-key") + /// builder.kind("org") + /// builder.trySetValue("firstName", "Pierre") + /// builder.trySetValue("lastName", "Menard") + /// builder.addPrivate(Reference("firstName")) + /// + /// let context = try builder.build().get() + /// ``` + /// + /// The attributes "kind", "key", and "anonymous" cannot be made private. + /// + /// This is a metadata property, rather than an attribute that can be addressed in evaluations: that is, + /// a rule clause that references the attribute name "private" will not use this value, but instead will + /// use whatever value (if any) you have set for that name with `trySetValue(...)`. + /// + /// # Designating an entire attribute as private + /// + /// If the parameter is an attribute name such as "email" that does not start with a '/' character, the + /// entire attribute is private. + /// + /// # Designating a property within a JSON object as private + /// + /// If the parameter starts with a '/' character, it is interpreted as a slash-delimited path to a + /// property within a JSON object. The first path component is an attribute name, and each following + /// component is a property name. + /// + /// For instance, suppose that the attribute "address" had the following JSON object value: + /// {"street": {"line1": "abc", "line2": "def"}, "city": "ghi"} + /// + /// - Calling either addPrivateAttribute(Reference("address")) or addPrivateAddress(Reference("/address")) would + /// cause the entire "address" attribute to be private. + /// - Calling addPrivateAttribute("/address/street") would cause the "street" property to be private, so that + /// only {"city": "ghi"} is included in analytics. + /// - Calling addPrivateAttribute("/address/street/line2") would cause only "line2" within "street" to be private, + /// so that {"street": {"line1": "abc"}, "city": "ghi"} is included in analytics. + /// + /// This syntax deliberately resembles JSON Pointer, but other JSON Pointer features such as array + /// indexing are not supported. + /// + /// 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". + @objc public func addPrivateAttribute(reference: ObjcLDReference) { builder.addPrivateAttribute(reference.reference) } + /// Remove any reference provided through `addPrivateAttribute(_:)`. If the reference was + /// added more than once, this method will remove all instances of it. + @objc public func removePrivateAttribute(reference: ObjcLDReference) { builder.removePrivateAttribute(reference.reference) } + + /// Sets the value of any attribute for the Context except for private attributes. + /// + /// This method uses the `LDValue` type to represent a value of any JSON type: null, + /// boolean, number, string, array, or object. For all attribute names that do not have special + /// meaning to LaunchDarkly, you may use any of those types. Values of different JSON types are + /// always treated as different values: for instance, null, false, and the empty string "" are + /// not the same, and the number 1 is not the same as the string "1". + /// + /// The following attribute names have special restrictions on their value types, and any value + /// of an unsupported type will be ignored (leaving the attribute unchanged): + /// + /// - "kind", "key": Must be a string. See `LDContextBuilder.kind(_:)` and `LDContextBuilder.key(_:)`. + /// + /// - "name": Must be a string or null. See `LDContextBuilder.name(_:)`. + /// + /// - "anonymous": Must be a boolean. See `LDContextBuilder.anonymous(_:)`. + /// + /// Values that are JSON arrays or objects have special behavior when referenced in + /// flag/segment rules. + /// + /// A value of `LDValue.null` is equivalent to removing any current non-default value + /// of the attribute. Null is not a valid attribute value in the LaunchDarkly model; any + /// expressions in feature flags that reference an attribute with a null value will behave as + /// if the attribute did not exist. + /// + /// This method returns true for success, or false if the parameters + /// violated one of the restrictions described above (for instance, + /// attempting to set "key" to a value that was not a string). + @discardableResult + @objc public func trySetValue(name: String, value: ObjcLDValue) -> Bool { + builder.trySetValue(name, value.wrappedValue) + } + + /// Creates a LDContext from the current LDContextBuilder properties. + /// + /// The LDContext is immutable and will not be affected by any subsequent actions on the + /// LDContextBuilder. + /// + /// It is possible to specify invalid attributes for a LDContextBuilder, such as an empty key. + /// In those situations, this method returns a Result.failure + @objc public func build() -> ContextBuilderResult { + switch builder.build() { + case .success(let context): + return ContextBuilderResult.fromSuccess(context) + case .failure(let error): + return ContextBuilderResult.fromError(error) + } + } +} + +/// Contains method for building a multi-context. +/// +/// Use this type if you need to construct a LDContext that has multiple kind values, each with its +/// own nested LDContext. To define a single-kind context, use `LDContextBuilder` instead. +/// +/// Obtain an instance of LDMultiContextBuilder by calling `LDMultiContextBuilder.init()`; then, call +/// `LDMultiContextBuilder.addContext(_:)` to specify the nested LDContext for each kind. +/// LDMultiContextBuilder setters return a reference the same builder, so they can be chained +/// together. +@objc(LDMultiContextBuilder) +public final class ObjcLDMultiContextBuilder: NSObject { + var builder: LDMultiContextBuilder + + @objc public override init() { + builder = LDMultiContextBuilder() + } + + /// Adds a nested context for a specific kind to a LDMultiContextBuilder. + /// + /// It is invalid to add more than one context with the same Kind. This error is detected when + /// you call `LDMultiContextBuilder.build()`. + @objc public func addContext(context: ObjcLDContext) { + builder.addContext(context.context) + } + + // Initializer to wrap the Swift LDMultiContextBuilder into ObjcLDMultiContextBuilder for use in + // Objective-C apps. + init(_ builder: LDMultiContextBuilder) { + self.builder = builder + } + + /// Creates a LDContext from the current properties. + /// + /// The LDContext is immutable and will not be affected by any subsequent actions on the + /// LDMultiContextBuilder. + /// + /// It is possible for a LDMultiContextBuilder to represent an invalid state. In those + /// situations, a Result.failure will be returned. + /// + /// If only one context kind was added to the builder, `build` returns a single-kind context rather + /// than a multi-context. + @objc public func build() -> ContextBuilderResult { + switch builder.build() { + case .success(let context): + return ContextBuilderResult.fromSuccess(context) + case .failure(let error): + return ContextBuilderResult.fromError(error) + } + } +} + +/// An NSObject which mimics Swift's Result type, specifically for the `LDContext` type. +@objc public class ContextBuilderResult: NSObject { + @objc public private(set) var success: ObjcLDContext? + @objc public private(set) var failure: NSError? + + private override init() { + super.init() + success = nil + failure = nil + } + + /// Create a "success" result with the provided `LDContext`. + public static func fromSuccess(_ success: LDContext) -> ContextBuilderResult { + ContextBuilderResult(success, nil) + } + + /// Create an "error" result with the provided `LDContext`. + public static func fromError(_ error: ContextBuilderError) -> ContextBuilderResult { + ContextBuilderResult(nil, error) + } + + private convenience init(_ arg1: LDContext?, _ arg2: ContextBuilderError?) { + self.init() + success = arg1.map { ObjcLDContext($0) } + failure = arg2.map { $0 as NSError } + } +} diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift index 3088f09a..38880f68 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift @@ -3,13 +3,13 @@ import Foundation /// Structure that contains the evaluation result and additional information when evaluating a flag as a boolean. @objc(LDBoolEvaluationDetail) public final class ObjcLDBoolEvaluationDetail: NSObject { - /// The value of the flag for the current user. + /// The value of the flag for the current context. @objc public let value: Bool /// The index of the returned value within the flag's list of variations, or `-1` if the default was returned. @objc public let variationIndex: Int /// A structure representing the main factor that influenced the resultant flag evaluation value. @objc public let reason: [String: ObjcLDValue]? - + internal init(value: Bool, variationIndex: Int?, reason: [String: ObjcLDValue]?) { self.value = value self.variationIndex = variationIndex ?? -1 @@ -20,13 +20,13 @@ public final class ObjcLDBoolEvaluationDetail: NSObject { /// Structure that contains the evaluation result and additional information when evaluating a flag as a double. @objc(LDDoubleEvaluationDetail) public final class ObjcLDDoubleEvaluationDetail: NSObject { - /// The value of the flag for the current user. + /// The value of the flag for the current context. @objc public let value: Double /// The index of the returned value within the flag's list of variations, or `-1` if the default was returned. @objc public let variationIndex: Int /// A structure representing the main factor that influenced the resultant flag evaluation value. @objc public let reason: [String: ObjcLDValue]? - + internal init(value: Double, variationIndex: Int?, reason: [String: ObjcLDValue]?) { self.value = value self.variationIndex = variationIndex ?? -1 @@ -37,13 +37,13 @@ public final class ObjcLDDoubleEvaluationDetail: NSObject { /// Structure that contains the evaluation result and additional information when evaluating a flag as an integer. @objc(LDIntegerEvaluationDetail) public final class ObjcLDIntegerEvaluationDetail: NSObject { - /// The value of the flag for the current user. + /// The value of the flag for the current context. @objc public let value: Int /// The index of the returned value within the flag's list of variations, or `-1` if the default was returned. @objc public let variationIndex: Int /// A structure representing the main factor that influenced the resultant flag evaluation value. @objc public let reason: [String: ObjcLDValue]? - + internal init(value: Int, variationIndex: Int?, reason: [String: ObjcLDValue]?) { self.value = value self.variationIndex = variationIndex ?? -1 @@ -54,13 +54,13 @@ public final class ObjcLDIntegerEvaluationDetail: NSObject { /// Structure that contains the evaluation result and additional information when evaluating a flag as a string. @objc(LDStringEvaluationDetail) public final class ObjcLDStringEvaluationDetail: NSObject { - /// The value of the flag for the current user. + /// The value of the flag for the current context. @objc public let value: String? /// The index of the returned value within the flag's list of variations, or `-1` if the default was returned. @objc public let variationIndex: Int /// A structure representing the main factor that influenced the resultant flag evaluation value. @objc public let reason: [String: ObjcLDValue]? - + internal init(value: String?, variationIndex: Int?, reason: [String: ObjcLDValue]?) { self.value = value self.variationIndex = variationIndex ?? -1 @@ -71,7 +71,7 @@ public final class ObjcLDStringEvaluationDetail: NSObject { /// Structure that contains the evaluation result and additional information when evaluating a flag as a JSON value. @objc(LDJSONEvaluationDetail) public final class ObjcLDJSONEvaluationDetail: NSObject { - /// The value of the flag for the current user. + /// The value of the flag for the current context. @objc public let value: ObjcLDValue /// The index of the returned value within the flag's list of variations, or `-1` if the default was returned. @objc public let variationIndex: Int diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDReference.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDReference.swift new file mode 100644 index 00000000..5b1c7994 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDReference.swift @@ -0,0 +1,38 @@ +import Foundation + +@objc(Reference) +public final class ObjcLDReference: NSObject { + var reference: Reference + + @objc public init(value: String) { + reference = Reference(value) + } + + // Initializer to wrap the Swift Reference into ObjcLDReference for use in + // Objective-C apps. + init(_ reference: Reference) { + self.reference = reference + } + + @objc public func isValid() -> Bool { reference.isValid() } + + @objc public func getError() -> NSError? { + guard let error = reference.getError() + else { return nil } + + return error as NSError + } +} + +@objc(ReferenceError) +public final class ObjcLDReferenceError: NSObject { + var error: ReferenceError + + // Initializer to wrap the Swift ReferenceError into ObjcLDReferenceError for use in + // Objective-C apps. + init(_ error: ReferenceError) { + self.error = error + } + + override public var description: String { self.error.description } +} diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift index 68d531db..b9b41921 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift @@ -1,18 +1,15 @@ import Foundation /** - LDUser allows clients to collect information about users in order to refine the feature flag values sent to the SDK. For example, the client app may launch with the SDK defined anonymous user. As the user works with the client app, information may be collected as needed and sent to LaunchDarkly. The client app controls the information collected, which LaunchDarkly does not use except as the client directs to refine feature flags. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. + LDUser allows clients to collect information about users in order to refine the feature flag values sent to the SDK. - The SDK caches last known feature flags for use on app startup to provide continuity with the last app run. Provided the LDClient is online and can establish a connection with LaunchDarkly servers, cached information will only be used a very short time. Once the latest feature flags arrive at the SDK, the SDK no longer uses cached feature flags. The SDK retains feature flags on the last 5 client defined users. The SDK will retain feature flags until they are overwritten by a different user's feature flags, or until the user removes the app from the device. - - The SDK does not cache user information collected, except for the user key. The user key is used to identify the cached feature flags for that user. Client app developers should use caution not to use sensitive user information as the user-key. + The usage of LDUser is no longer recommended and is retained only to ease the adoption of the `LDContext` class. New + code using this SDK should make use of the `LDContextBuilder` to construct an equivalent `Kind.user` kind context. */ @objc (LDUser) public final class ObjcLDUser: NSObject { var user: LDUser - /// LDUser secondary attribute used to make `secondary` private - @objc public class var attributeSecondary: String { "secondary" } /// LDUser name attribute used to make `name` private @objc public class var attributeName: String { "name" } /// LDUser firstName attribute used to make `firstName` private @@ -32,11 +29,6 @@ public final class ObjcLDUser: NSObject { @objc public var key: String { return user.key } - /// The secondary key for the user. Read the [documentation](https://docs.launchdarkly.com/home/flags/rollouts) for more information on it's use for percentage rollout bucketing. - @objc public var secondary: String? { - get { user.secondary } - set { user.secondary = newValue } - } /// Client app defined name for the user. (Default: nil) @objc public var name: String? { get { user.name } @@ -125,4 +117,14 @@ public final class ObjcLDUser: NSObject { else { return false } return user == otherUser.user } + + /// Convert a legacy LDUser to the newer LDContext + @objc public func toContext() -> ContextBuilderResult { + switch self.user.toContext() { + case .success(let context): + return ContextBuilderResult.fromSuccess(context) + case .failure(let error): + return ContextBuilderResult.fromError(error) + } + } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift index 6e0d7e63..2ee646c5 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift @@ -2,7 +2,7 @@ import Foundation // sourcery: autoMockable protocol CacheConverting { - func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int) + func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedContexts: Int) } // Cache model in SDK versions >=4.0.0 <6.0.0. Migration is not supported for earlier versions. @@ -89,9 +89,9 @@ final class CacheConverter: CacheConverting { } } - cachedEnvData.forEach { mobileKey, users in - users.forEach { userKey, data in - flagCaches[mobileKey]?.storeFeatureFlags(StoredItems(items: data.flags), userKey: userKey, lastUpdated: data.updated) + cachedEnvData.forEach { mobileKey, contexts in + contexts.forEach { contextKey, data in + flagCaches[mobileKey]?.storeFeatureFlags(StoredItems(items: data.flags), contextKey: contextKey, lastUpdated: data.updated) } } @@ -101,6 +101,17 @@ final class CacheConverter: CacheConverting { private func convertV7Data(flagCaches: inout [MobileKey: FeatureFlagCaching]) { for (_, flagCaching) in flagCaches { flagCaching.keyedValueCache.keys().forEach { key in + // Deal with renaming context-users cache key + if key == "context-users" { + guard let data = flagCaching.keyedValueCache.data(forKey: "cached-users") + else { + return + } + flagCaching.keyedValueCache.removeObject(forKey: "cached-users") + flagCaching.keyedValueCache.set(data, forKey: "cached-contexts") + return + } + guard let cachedData = flagCaching.keyedValueCache.data(forKey: key), let cachedFlags = try? JSONDecoder().decode(FeatureFlagCollection.self, from: cachedData) else { return } @@ -113,10 +124,10 @@ final class CacheConverter: CacheConverting { } } - func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedUsers: Int) { + func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedContexts: Int) { var flagCaches: [String: FeatureFlagCaching] = [:] keysToConvert.forEach { mobileKey in - let flagCache = serviceFactory.makeFeatureFlagCache(mobileKey: mobileKey, maxCachedUsers: maxCachedUsers) + let flagCache = serviceFactory.makeFeatureFlagCache(mobileKey: mobileKey, maxCachedContexts: maxCachedContexts) flagCaches[mobileKey] = flagCache // Get current cache version and return if up to date guard let cacheVersionData = flagCache.keyedValueCache.data(forKey: "ld-cache-metadata") diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift index a49e0ee9..7984f7fb 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift @@ -5,15 +5,15 @@ protocol FeatureFlagCaching { // sourcery: defaultMockValue = KeyedValueCachingMock() var keyedValueCache: KeyedValueCaching { get } - func retrieveFeatureFlags(userKey: String) -> StoredItems? - func storeFeatureFlags(_ storedItems: StoredItems, userKey: String, lastUpdated: Date) + func retrieveFeatureFlags(contextKey: String) -> StoredItems? + func storeFeatureFlags(_ storedItems: StoredItems, contextKey: String, lastUpdated: Date) } final class FeatureFlagCache: FeatureFlagCaching { let keyedValueCache: KeyedValueCaching - let maxCachedUsers: Int + let maxCachedContexts: Int - init(serviceFactory: ClientServiceCreating, mobileKey: MobileKey, maxCachedUsers: Int) { + init(serviceFactory: ClientServiceCreating, mobileKey: MobileKey, maxCachedContexts: Int) { let cacheKey: String if let bundleId = Bundle.main.bundleIdentifier { cacheKey = "\(Util.sha256base64(bundleId)).\(Util.sha256base64(mobileKey))" @@ -21,37 +21,36 @@ final class FeatureFlagCache: FeatureFlagCaching { cacheKey = Util.sha256base64(mobileKey) } self.keyedValueCache = serviceFactory.makeKeyedValueCache(cacheKey: "com.launchdarkly.client.\(cacheKey)") - self.maxCachedUsers = maxCachedUsers + self.maxCachedContexts = maxCachedContexts } - func retrieveFeatureFlags(userKey: String) -> StoredItems? { - guard let cachedData = keyedValueCache.data(forKey: "flags-\(Util.sha256base64(userKey))"), + func retrieveFeatureFlags(contextKey: String) -> StoredItems? { + guard let cachedData = keyedValueCache.data(forKey: "flags-\(contextKey)"), let cachedFlags = try? JSONDecoder().decode(StoredItemCollection.self, from: cachedData) else { return nil } return cachedFlags.flags } - func storeFeatureFlags(_ storedItems: StoredItems, userKey: String, lastUpdated: Date) { - guard self.maxCachedUsers != 0, let encoded = try? JSONEncoder().encode(StoredItemCollection(storedItems)) + func storeFeatureFlags(_ storedItems: StoredItems, contextKey: String, lastUpdated: Date) { + guard self.maxCachedContexts != 0, let encoded = try? JSONEncoder().encode(StoredItemCollection(storedItems)) else { return } - let userSha = Util.sha256base64(userKey) - self.keyedValueCache.set(encoded, forKey: "flags-\(userSha)") + self.keyedValueCache.set(encoded, forKey: "flags-\(contextKey)") - var cachedUsers: [String: Int64] = [:] - if let cacheMetadata = self.keyedValueCache.data(forKey: "cached-users") { - cachedUsers = (try? JSONDecoder().decode([String: Int64].self, from: cacheMetadata)) ?? [:] + var cachedContexts: [String: Int64] = [:] + if let cacheMetadata = self.keyedValueCache.data(forKey: "cached-contexts") { + cachedContexts = (try? JSONDecoder().decode([String: Int64].self, from: cacheMetadata)) ?? [:] } - cachedUsers[userSha] = lastUpdated.millisSince1970 - if cachedUsers.count > self.maxCachedUsers && self.maxCachedUsers > 0 { - let sorted = cachedUsers.sorted { $0.value < $1.value } - sorted.prefix(cachedUsers.count - self.maxCachedUsers).forEach { sha, _ in - cachedUsers.removeValue(forKey: sha) + cachedContexts[contextKey] = lastUpdated.millisSince1970 + if cachedContexts.count > self.maxCachedContexts && self.maxCachedContexts > 0 { + let sorted = cachedContexts.sorted { $0.value < $1.value } + sorted.prefix(cachedContexts.count - self.maxCachedContexts).forEach { sha, _ in + cachedContexts.removeValue(forKey: sha) self.keyedValueCache.removeObject(forKey: "flags-\(sha)") } } - if let encoded = try? JSONEncoder().encode(cachedUsers) { - self.keyedValueCache.set(encoded, forKey: "cached-users") + if let encoded = try? JSONEncoder().encode(cachedContexts) { + self.keyedValueCache.set(encoded, forKey: "cached-contexts") } } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift index aa804f8a..baf0784f 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift @@ -3,9 +3,9 @@ import LDSwiftEventSource protocol ClientServiceCreating { func makeKeyedValueCache(cacheKey: String?) -> KeyedValueCaching - func makeFeatureFlagCache(mobileKey: String, maxCachedUsers: Int) -> FeatureFlagCaching + func makeFeatureFlagCache(mobileKey: String, maxCachedContexts: Int) -> FeatureFlagCaching func makeCacheConverter() -> CacheConverting - func makeDarklyServiceProvider(config: LDConfig, user: LDUser) -> DarklyServiceProvider + func makeDarklyServiceProvider(config: LDConfig, context: LDContext) -> DarklyServiceProvider func makeFlagSynchronizer(streamingMode: LDStreamingMode, pollingInterval: TimeInterval, useReport: Bool, service: DarklyServiceProvider) -> LDFlagSynchronizing func makeFlagSynchronizer(streamingMode: LDStreamingMode, pollingInterval: TimeInterval, @@ -29,16 +29,16 @@ final class ClientServiceFactory: ClientServiceCreating { UserDefaults(suiteName: cacheKey)! } - func makeFeatureFlagCache(mobileKey: MobileKey, maxCachedUsers: Int) -> FeatureFlagCaching { - FeatureFlagCache(serviceFactory: self, mobileKey: mobileKey, maxCachedUsers: maxCachedUsers) + func makeFeatureFlagCache(mobileKey: MobileKey, maxCachedContexts: Int) -> FeatureFlagCaching { + FeatureFlagCache(serviceFactory: self, mobileKey: mobileKey, maxCachedContexts: maxCachedContexts) } func makeCacheConverter() -> CacheConverting { CacheConverter() } - func makeDarklyServiceProvider(config: LDConfig, user: LDUser) -> DarklyServiceProvider { - DarklyService(config: config, user: user, serviceFactory: self) + func makeDarklyServiceProvider(config: LDConfig, context: LDContext) -> DarklyServiceProvider { + DarklyService(config: config, context: context, serviceFactory: self) } func makeFlagSynchronizer(streamingMode: LDStreamingMode, pollingInterval: TimeInterval, useReport: Bool, service: DarklyServiceProvider) -> LDFlagSynchronizing { @@ -65,11 +65,11 @@ final class ClientServiceFactory: ClientServiceCreating { EventReporter(service: service, onSyncComplete: onSyncComplete) } - func makeStreamingProvider(url: URL, - httpHeaders: [String: String], + func makeStreamingProvider(url: URL, + httpHeaders: [String: String], connectMethod: String, - connectBody: Data?, - handler: EventHandler, + connectBody: Data?, + handler: EventHandler, delegate: RequestHeaderTransform?, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider { var config: EventSource.Config = EventSource.Config(handler: handler, url: url) @@ -92,7 +92,7 @@ final class ClientServiceFactory: ClientServiceCreating { func makeThrottler(environmentReporter: EnvironmentReporting) -> Throttling { Throttler(environmentReporter: environmentReporter) } - + func makeConnectionInformation() -> ConnectionInformation { ConnectionInformation(currentConnectionMode: .offline, lastConnectionFailureReason: .none) } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift index 89125d99..7446ac53 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift @@ -38,8 +38,6 @@ protocol EnvironmentReporting { var isDebugBuild: Bool { get } // sourcery: defaultMockValue = Constants.deviceType var deviceType: String { get } - // sourcery: defaultMockValue = Constants.deviceModel - var deviceModel: String { get } // sourcery: defaultMockValue = Constants.systemVersion var systemVersion: String { get } // sourcery: defaultMockValue = Constants.systemName @@ -69,25 +67,6 @@ struct EnvironmentReporter: EnvironmentReporting { fileprivate static let simulatorModelIdentifier = "SIMULATOR_MODEL_IDENTIFIER" } - var deviceModel: String { - #if os(OSX) - return Sysctl.model - #else - // Obtaining the device model from https://stackoverflow.com/questions/26028918/how-to-determine-the-current-iphone-device-model answer by Jens Schwarzer - if let simulatorModelIdentifier = ProcessInfo().environment[Constants.simulatorModelIdentifier] { - return simulatorModelIdentifier - } - // the physical device code here is not automatically testable. Manual testing on physical devices is required. - var systemInfo = utsname() - _ = uname(&systemInfo) - guard let deviceModel = String(bytes: Data(bytes: &systemInfo.machine, count: Int(_SYS_NAMELEN)), encoding: .ascii) - else { - return deviceType - } - return deviceModel.trimmingCharacters(in: .controlCharacters) - #endif - } - #if os(iOS) var deviceType: String { UIDevice.current.model } var systemVersion: String { UIDevice.current.systemVersion } @@ -123,7 +102,7 @@ struct EnvironmentReporter: EnvironmentReporting { #endif var shouldThrottleOnlineCalls: Bool { !isDebugBuild } - let sdkVersion = "7.1.0" + let sdkVersion = "8.0.0" // Unfortunately, the following does not function in certain configurations, such as when included through SPM // var sdkVersion: String { // Bundle(for: LDClient.self).infoDictionary?["CFBundleShortVersionString"] as? String ?? "5.x" diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift index 8d746de1..73ea2104 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift @@ -5,11 +5,12 @@ typealias EventSyncCompleteClosure = ((SynchronizingError?) -> Void) protocol EventReporting { // sourcery: defaultMockValue = false var isOnline: Bool { get set } - var lastEventResponseDate: Date? { get } + // sourcery: defaultMockValue = Date.distantPast + var lastEventResponseDate: Date { get } func record(_ event: Event) // swiftlint:disable:next function_parameter_count - func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) + func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, context: LDContext, includeReason: Bool) func flush(completion: CompletionClosure?) } @@ -19,7 +20,7 @@ class EventReporter: EventReporting { set { timerQueue.sync { newValue ? startReporting() : stopReporting() } } } - private (set) var lastEventResponseDate: Date? + private (set) var lastEventResponseDate: Date let service: DarklyServiceProvider @@ -37,6 +38,7 @@ class EventReporter: EventReporting { init(service: DarklyServiceProvider, onSyncComplete: EventSyncCompleteClosure?) { self.service = service self.onSyncComplete = onSyncComplete + self.lastEventResponseDate = Date() } func record(_ event: Event) { @@ -54,18 +56,18 @@ class EventReporter: EventReporting { } // swiftlint:disable:next function_parameter_count - func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) { + func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, context: LDContext, includeReason: Bool) { let recordingFeatureEvent = featureFlag?.trackEvents == true let recordingDebugEvent = featureFlag?.shouldCreateDebugEvents(lastEventReportResponseTime: lastEventResponseDate) ?? false eventQueue.sync { - flagRequestTracker.trackRequest(flagKey: flagKey, reportedValue: value, featureFlag: featureFlag, defaultValue: defaultValue) + flagRequestTracker.trackRequest(flagKey: flagKey, reportedValue: value, featureFlag: featureFlag, defaultValue: defaultValue, context: context) if recordingFeatureEvent { - let featureEvent = FeatureEvent(key: flagKey, user: user, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason, isDebug: false) + let featureEvent = FeatureEvent(key: flagKey, context: context, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason, isDebug: false) recordNoSync(featureEvent) } if recordingDebugEvent { - let debugEvent = FeatureEvent(key: flagKey, user: user, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason, isDebug: true) + let debugEvent = FeatureEvent(key: flagKey, context: context, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason, isDebug: true) recordNoSync(debugEvent) } } @@ -76,7 +78,7 @@ class EventReporter: EventReporting { else { return } eventReportTimer = LDTimer(withTimeInterval: service.config.eventFlushInterval, fireQueue: eventQueue, execute: reportEvents) } - + private func stopReporting() { eventReportTimer?.cancel() eventReportTimer = nil @@ -128,9 +130,10 @@ class EventReporter: EventReporting { private func publish(_ events: [Event], _ payloadId: String, _ completion: CompletionClosure?) { let encodingConfig: [CodingUserInfoKey: Any] = - [Event.UserInfoKeys.inlineUserInEvents: service.config.inlineUserInEvents, - LDUser.UserInfoKeys.allAttributesPrivate: service.config.allUserAttributesPrivate, - LDUser.UserInfoKeys.globalPrivateAttributes: service.config.privateUserAttributes.map { $0.name }] + [ + LDContext.UserInfoKeys.allAttributesPrivate: service.config.allContextAttributesPrivate, + LDContext.UserInfoKeys.globalPrivateAttributes: service.config.privateContextAttributes.map { $0 } + ] let encoder = JSONEncoder() encoder.userInfo = encodingConfig encoder.dateEncodingStrategy = .custom { date, encoder in @@ -161,7 +164,11 @@ class EventReporter: EventReporting { private func processEventResponse(sentEvents: Int, response: HTTPURLResponse?, error: Error?, isRetry: Bool) -> Bool { if error == nil && (200..<300).contains(response?.statusCode ?? 0) { - self.lastEventResponseDate = response?.headerDate ?? self.lastEventResponseDate + let serverTime = response?.headerDate ?? self.lastEventResponseDate + if serverTime > self.lastEventResponseDate { + self.lastEventResponseDate = serverTime + } + Log.debug(self.typeName(and: #function) + "Completed sending \(sentEvents) event(s)") self.reportSyncComplete(nil) return false @@ -202,7 +209,7 @@ extension EventReporter: TypeIdentifying { } #if DEBUG extension EventReporter { - func setLastEventResponseDate(_ date: Date?) { + func setLastEventResponseDate(_ date: Date) { lastEventResponseDate = date } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift index 6789c7c6..d4bbc42d 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift @@ -78,13 +78,13 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { private var isOnlineQueue = DispatchQueue(label: "com.launchdarkly.FlagSynchronizer.isOnlineQueue") let pollingInterval: TimeInterval let useReport: Bool - + private var syncQueue = DispatchQueue(label: Constants.queueName, qos: .utility) private var eventSourceStarted: Date? - init(streamingMode: LDStreamingMode, - pollingInterval: TimeInterval, - useReport: Bool, + init(streamingMode: LDStreamingMode, + pollingInterval: TimeInterval, + useReport: Bool, service: DarklyServiceProvider, onSyncComplete: FlagSyncCompleteClosure?) { Log.debug(FlagSynchronizer.typeName(and: #function) + "streamingMode: \(streamingMode), " + "pollingInterval: \(pollingInterval), " + "useReport: \(useReport)") @@ -226,7 +226,7 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { onSyncComplete(result) } } - + // sourcery: noMock deinit { onSyncComplete = nil diff --git a/LaunchDarkly/LaunchDarkly/Util.swift b/LaunchDarkly/LaunchDarkly/Util.swift index 7ecf2a2b..fd7eea02 100644 --- a/LaunchDarkly/LaunchDarkly/Util.swift +++ b/LaunchDarkly/LaunchDarkly/Util.swift @@ -2,6 +2,9 @@ import CommonCrypto import Foundation class Util { + internal static let validKindCharacterSet = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-") + internal static let validTagCharacterSet = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-") + class func sha256base64(_ str: String) -> String { let data = Data(str.utf8) var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) @@ -11,3 +14,13 @@ class Util { return Data(digest).base64EncodedString() } } + +extension String { + func onlyContainsCharset(_ set: CharacterSet) -> Bool { + if description.rangeOfCharacter(from: set.inverted) != nil { + return false + } + + return true + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index 9d9475a9..4b8a5d94 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -23,10 +23,10 @@ final class LDClientSpec: QuickSpec { class TestContext { var config: LDConfig! - var user: LDUser! + var context: LDContext! var subject: LDClient! let serviceFactoryMock = ClientServiceMockFactory() - // mock getters based on setting up the user & subject + // mock getters based on setting up the context & subject var serviceMock: DarklyServiceMock! { subject.service as? DarklyServiceMock } @@ -73,8 +73,7 @@ final class LDClientSpec: QuickSpec { startOnline: Bool = false, streamingMode: LDStreamingMode = .streaming, enableBackgroundUpdates: Bool = true, - operatingSystem: OperatingSystem? = nil, - autoAliasingOptOut: Bool = true) { + operatingSystem: OperatingSystem? = nil) { if let operatingSystem = operatingSystem { serviceFactoryMock.makeEnvironmentReporterReturnValue.operatingSystem = operatingSystem @@ -85,7 +84,7 @@ final class LDClientSpec: QuickSpec { let mobileKey = self.serviceFactoryMock.makeFeatureFlagCacheReceivedParameters!.mobileKey let mockCache = FeatureFlagCachingMock() mockCache.retrieveFeatureFlagsCallback = { - mockCache.retrieveFeatureFlagsReturnValue = StoredItems(items: self.cachedFlags[mobileKey]?[mockCache.retrieveFeatureFlagsReceivedUserKey!] ?? [:]) + mockCache.retrieveFeatureFlagsReturnValue = StoredItems(items: self.cachedFlags[mobileKey]?[mockCache.retrieveFeatureFlagsReceivedContextKey!] ?? [:]) } self.serviceFactoryMock.makeFeatureFlagCacheReturnValue = mockCache } @@ -95,29 +94,28 @@ final class LDClientSpec: QuickSpec { config.streamingMode = streamingMode config.enableBackgroundUpdates = enableBackgroundUpdates config.eventFlushInterval = 300.0 // 5 min...don't want this to trigger - config.autoAliasingOptOut = autoAliasingOptOut - user = LDUser.stub() + context = LDContext.stub() } - func withUser(_ user: LDUser?) -> TestContext { - self.user = user + func withContext(_ context: LDContext?) -> TestContext { + self.context = context return self } func withCached(flags: [LDFlagKey: FeatureFlag]?) -> TestContext { - withCached(userKey: user.key, flags: flags) + withCached(contextKey: context.fullyQualifiedHashedKey(), flags: flags) } - func withCached(userKey: String, flags: [LDFlagKey: FeatureFlag]?) -> TestContext { + func withCached(contextKey: String, flags: [LDFlagKey: FeatureFlag]?) -> TestContext { var forEnv = cachedFlags[config.mobileKey] ?? [:] - forEnv[userKey] = flags + forEnv[contextKey] = flags cachedFlags[config.mobileKey] = forEnv return self } func start(runMode: LDClientRunMode = .foreground, completion: (() -> Void)? = nil) { - LDClient.start(serviceFactory: serviceFactoryMock, config: config, user: user) { + LDClient.start(serviceFactory: serviceFactoryMock, config: config, context: context) { self.subject = LDClient.get() if runMode == .background { self.subject.setRunMode(.background) @@ -128,7 +126,7 @@ final class LDClientSpec: QuickSpec { } func start(runMode: LDClientRunMode = .foreground, timeOut: TimeInterval, timeOutCompletion: ((_ timedOut: Bool) -> Void)? = nil) { - LDClient.start(serviceFactory: serviceFactoryMock, config: config, user: user, startWaitSeconds: timeOut) { timedOut in + LDClient.start(serviceFactory: serviceFactoryMock, config: config, context: context, startWaitSeconds: timeOut) { timedOut in self.subject = LDClient.get() if runMode == .background { self.subject.setRunMode(.background) @@ -155,41 +153,9 @@ final class LDClientSpec: QuickSpec { allFlagsSpec() connectionInformationSpec() variationDetailSpec() - aliasingSpec() isInitializedSpec() } - private func aliasingSpec() { - let anonUser = LDUser(key: "unknown", isAnonymous: true) - let knownUser = LDUser(key: "known", isAnonymous: false) - describe("aliasing") { - it("automatic aliasing from anonymous to user") { - let ctx = TestContext(autoAliasingOptOut: false) - ctx.withUser(anonUser).start() - ctx.subject.internalIdentify(newUser: knownUser) - // init, identify, and alias event - expect(ctx.eventReporterMock.recordCallCount) == 3 - expect(ctx.recordedEvent?.kind) == .alias - } - it("no automatic aliasing from user to user") { - let ctx = TestContext(autoAliasingOptOut: false) - ctx.withUser(knownUser).start() - ctx.subject.internalIdentify(newUser: knownUser) - // init and identify event - expect(ctx.eventReporterMock.recordCallCount) == 2 - expect(ctx.recordedEvent?.kind) == .identify - } - it("no automatic aliasing from anonymous to anonymous") { - let ctx = TestContext(autoAliasingOptOut: false) - ctx.withUser(anonUser).start() - ctx.subject.internalIdentify(newUser: anonUser) - // init and identify event - expect(ctx.eventReporterMock.recordCallCount) == 2 - expect(ctx.recordedEvent?.kind) == .identify - } - } - } - private func startSpec() { describe("start") { startSpec(withTimeout: false) @@ -222,26 +188,26 @@ final class LDClientSpec: QuickSpec { expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: testContext.subject.runMode) expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } - it("saves the user") { - expect(testContext.subject.user) == testContext.user - expect(testContext.subject.service.user) == testContext.user + it("saves the context") { + expect(testContext.subject.context) == testContext.context + expect(testContext.subject.service.context) == testContext.context expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { expect(makeFlagSynchronizerReceivedParameters.service) === testContext.subject.service } - expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.user) == testContext.user + expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } - it("uncaches the new users flags") { + it("uncaches the new contexts flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 - expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user + expect((testContext.recordedEvent as? IdentifyEvent)?.context) == testContext.context } it("converts cached data") { expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 - expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedContexts) == testContext.config.maxCachedContexts expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } it("starts in foreground") { @@ -265,40 +231,40 @@ final class LDClientSpec: QuickSpec { expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: testContext.subject.runMode) expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } - it("saves the user") { - expect(testContext.subject.user) == testContext.user - expect(testContext.subject.service.user) == testContext.user + it("saves the context") { + expect(testContext.subject.context) == testContext.context + expect(testContext.subject.service.context) == testContext.context expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { expect(makeFlagSynchronizerReceivedParameters.service) === testContext.subject.service } - expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.user) == testContext.user + expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } - it("uncaches the new users flags") { + it("uncaches the new contexts flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 - expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user + expect((testContext.recordedEvent as? IdentifyEvent)?.context) == testContext.context } it("converts cached data") { expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 - expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedContexts) == testContext.config.maxCachedContexts expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } it("starts in foreground") { expect(testContext.subject.runMode) == .foreground } } - context("when called without user") { - context("after setting user") { + context("when called without context") { + context("after setting context") { beforeEach { - testContext = TestContext(startOnline: true).withUser(nil) + testContext = TestContext(startOnline: true).withContext(nil) withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() - testContext.user = LDUser.stub() - testContext.subject.internalIdentify(newUser: testContext.user) + testContext.context = LDContext.stub() + testContext.subject.internalIdentify(newContext: testContext.context) } it("saves the config") { expect(testContext.subject.config) == testContext.config @@ -307,32 +273,32 @@ final class LDClientSpec: QuickSpec { expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: testContext.subject.runMode) expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } - it("saves the user") { - expect(testContext.subject.user) == testContext.user - expect(testContext.subject.service.user) == testContext.user + it("saves the context") { + expect(testContext.subject.context) == testContext.context + expect(testContext.subject.service.context) == testContext.context expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { - expect(makeFlagSynchronizerReceivedParameters.service.user) == testContext.user + expect(makeFlagSynchronizerReceivedParameters.service.context) == testContext.context } - expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.user) == testContext.user + expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } - it("uncaches the new users flags") { + it("uncaches the new contexts flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 2 // called on init and subsequent identify - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 2 // both start and internalIdentify - expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user + expect((testContext.recordedEvent as? IdentifyEvent)?.context) == testContext.context } it("converts cached data") { expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 - expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedContexts) == testContext.config.maxCachedContexts expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } } - context("without setting user") { + context("without setting context") { beforeEach { - testContext = TestContext(startOnline: true).withUser(nil) + testContext = TestContext(startOnline: true).withContext(nil) withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() } it("saves the config") { @@ -342,53 +308,52 @@ final class LDClientSpec: QuickSpec { expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: testContext.subject.runMode) expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } - it("uses anonymous user") { - expect(testContext.subject.user.key) == LDUser.defaultKey(environmentReporter: testContext.environmentReporterMock) - expect(testContext.subject.user.isAnonymous).to(beTrue()) - expect(testContext.subject.service.user) == testContext.subject.user - expect(testContext.makeFlagSynchronizerService?.user) == testContext.subject.user - expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.user) == testContext.subject.user + it("uses anonymous context") { + expect(testContext.subject.context.fullyQualifiedKey()) == LDContext.defaultKey(kind: testContext.subject.context.kind) + expect(testContext.subject.service.context) == testContext.subject.context + expect(testContext.makeFlagSynchronizerService?.context) == testContext.subject.context + expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.subject.context } - it("uncaches the new users flags") { + it("uncaches the new contexts flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.subject.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.subject.context.fullyQualifiedHashedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 - expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.subject.user + expect((testContext.recordedEvent as? IdentifyEvent)?.context) == testContext.subject.context } it("converts cached data") { expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 - expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedContexts) == testContext.config.maxCachedContexts expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } } } - it("when called with cached flags for the user and environment") { + it("when called with cached flags for the context and environment") { let cachedFlags = ["test-flag": StorageItem.item(FeatureFlag(flagKey: "test-flag"))] let testContext = TestContext().withCached(flags: cachedFlags.featureFlags) withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags) == cachedFlags expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 - expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedContexts) == testContext.config.maxCachedContexts expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } - it("when called without cached flags for the user") { + it("when called without cached flags for the context") { let testContext = TestContext() withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() expect(testContext.flagStoreMock.replaceStoreCallCount) == 0 expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataCallCount) == 1 - expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedUsers) == testContext.config.maxCachedUsers + expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.maxCachedContexts) == testContext.config.maxCachedContexts expect(testContext.serviceFactoryMock.makeCacheConverterReturnValue.convertCacheDataReceivedArguments?.keysToConvert) == [testContext.config.mobileKey] } } @@ -506,22 +471,22 @@ final class LDClientSpec: QuickSpec { expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: .background) expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } - it("saves the user") { - expect(testContext.subject.user) == testContext.user - expect(testContext.subject.service.user) == testContext.user + it("saves the context") { + expect(testContext.subject.context) == testContext.context + expect(testContext.subject.service.context) == testContext.context expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { expect(makeFlagSynchronizerReceivedParameters.service) === testContext.subject.service } - expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.user) == testContext.user + expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } - it("uncaches the new users flags") { + it("uncaches the new contexts flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 - expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user + expect((testContext.recordedEvent as? IdentifyEvent)?.context) == testContext.context } } } @@ -545,22 +510,22 @@ final class LDClientSpec: QuickSpec { expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: .background) expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } - it("saves the user") { - expect(testContext.subject.user) == testContext.user - expect(testContext.subject.service.user) == testContext.user + it("saves the context") { + expect(testContext.subject.context) == testContext.context + expect(testContext.subject.service.context) == testContext.context expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { - expect(makeFlagSynchronizerReceivedParameters.service.user) == testContext.user + expect(makeFlagSynchronizerReceivedParameters.service.context) == testContext.context } - expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.user) == testContext.user + expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } - it("uncaches the new users flags") { + it("uncaches the new contexts flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == testContext.context.fullyQualifiedHashedKey() } it("records an identify event") { expect(testContext.eventReporterMock.recordCallCount) == 1 - expect((testContext.recordedEvent as? IdentifyEvent)?.user) == testContext.user + expect((testContext.recordedEvent as? IdentifyEvent)?.context) == testContext.context } } } @@ -575,20 +540,20 @@ final class LDClientSpec: QuickSpec { testContext.start() testContext.featureFlagCachingMock.reset() - let newUser = LDUser.stub() - testContext.subject.internalIdentify(newUser: newUser) + let newContext = LDContext.stub() + testContext.subject.internalIdentify(newContext: newContext) - expect(testContext.subject.user) == newUser - expect(testContext.subject.service.user) == newUser + expect(testContext.subject.context) == newContext + expect(testContext.subject.service.context) == newContext expect(testContext.serviceMock.clearFlagResponseCacheCallCount) == 1 - expect(testContext.makeFlagSynchronizerService?.user) == newUser + expect(testContext.makeFlagSynchronizerService?.context) == newContext expect(testContext.subject.isOnline) == true expect(testContext.subject.eventReporter.isOnline) == true expect(testContext.subject.flagSynchronizer.isOnline) == true expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == newUser.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == newContext.fullyQualifiedHashedKey() expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) } @@ -597,33 +562,33 @@ final class LDClientSpec: QuickSpec { testContext.start() testContext.featureFlagCachingMock.reset() - let newUser = LDUser.stub() - testContext.subject.internalIdentify(newUser: newUser) + let newContext = LDContext.stub() + testContext.subject.internalIdentify(newContext: newContext) - expect(testContext.subject.user) == newUser - expect(testContext.subject.service.user) == newUser + expect(testContext.subject.context) == newContext + expect(testContext.subject.service.context) == newContext expect(testContext.serviceMock.clearFlagResponseCacheCallCount) == 1 - expect(testContext.makeFlagSynchronizerService?.user) == newUser + expect(testContext.makeFlagSynchronizerService?.context) == newContext expect(testContext.subject.isOnline) == false expect(testContext.subject.eventReporter.isOnline) == false expect(testContext.subject.flagSynchronizer.isOnline) == false expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedUserKey) == newUser.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedContextKey) == newContext.fullyQualifiedHashedKey() expect(testContext.eventReporterMock.recordReceivedEvent?.kind == .identify).to(beTrue()) } - it("when the new user has cached feature flags") { + it("when the new context has cached feature flags") { let stubFlags = FlagMaintainingMock.stubStoredItems() - let newUser = LDUser.stub() - let testContext = TestContext().withCached(userKey: newUser.key, flags: stubFlags.featureFlags) + let newContext = LDContext.stub() + let testContext = TestContext().withCached(contextKey: newContext.fullyQualifiedHashedKey(), flags: stubFlags.featureFlags) testContext.start() testContext.featureFlagCachingMock.reset() - testContext.subject.internalIdentify(newUser: newUser) + testContext.subject.internalIdentify(newContext: newContext) - expect(testContext.subject.user) == newUser + expect(testContext.subject.context) == newContext expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags) == stubFlags } @@ -738,7 +703,7 @@ final class LDClientSpec: QuickSpec { testContext.subject.track(key: "customEvent", data: "abc", metricValue: 5.0) let receivedEvent = testContext.eventReporterMock.recordReceivedEvent as? CustomEvent expect(receivedEvent?.key) == "customEvent" - expect(receivedEvent?.user) == testContext.user + expect(receivedEvent?.context) == testContext.context expect(receivedEvent?.data) == "abc" expect(receivedEvent?.metricValue) == 5.0 } @@ -782,7 +747,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value) == DarklyServiceMock.FlagValues.bool expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue) == .bool(DefaultFlagValues.bool) expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.featureFlag) == testContext.flagStoreMock.storedItems.featureFlags[DarklyServiceMock.FlagKeys.bool] - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.user) == testContext.user + expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.context) == testContext.context } } } @@ -803,7 +768,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value) == .bool(DefaultFlagValues.bool) expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue) == .bool(DefaultFlagValues.bool) expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.featureFlag).to(beNil()) - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.user) == testContext.user + expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.context) == testContext.context } } } @@ -912,7 +877,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storedItems) == newStoredItems - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.contextKey) == testContext.context.fullyQualifiedHashedKey() expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 @@ -939,7 +904,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storedItems) == testContext.flagStoreMock.storedItems - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.contextKey) == testContext.context.fullyQualifiedHashedKey() expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 @@ -966,7 +931,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storedItems.featureFlags) == testContext.flagStoreMock.storedItems.featureFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.contextKey) == testContext.context.fullyQualifiedHashedKey() expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated).to(beCloseTo(updateDate, within: Constants.updateThreshold)) expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 @@ -1311,7 +1276,7 @@ final class LDClientSpec: QuickSpec { } } } - + private func connectionInformationSpec() { describe("ConnectionInformation") { it("when client was started in foreground") { @@ -1333,7 +1298,7 @@ final class LDClientSpec: QuickSpec { } } } - + private func variationDetailSpec() { describe("variationDetail") { it("when flag doesn't exist") { @@ -1384,7 +1349,7 @@ final class LDClientSpec: QuickSpec { extension FeatureFlagCachingMock { func reset() { retrieveFeatureFlagsCallCount = 0 - retrieveFeatureFlagsReceivedUserKey = nil + retrieveFeatureFlagsReceivedContextKey = nil retrieveFeatureFlagsReturnValue = nil storeFeatureFlagsCallCount = 0 storeFeatureFlagsReceivedArguments = nil diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift index e41a117a..d62c5761 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift @@ -15,10 +15,10 @@ final class ClientServiceMockFactory: ClientServiceCreating { var makeFeatureFlagCacheReturnValue = FeatureFlagCachingMock() var makeFeatureFlagCacheCallback: (() -> Void)? var makeFeatureFlagCacheCallCount = 0 - var makeFeatureFlagCacheReceivedParameters: (mobileKey: MobileKey, maxCachedUsers: Int)? = nil - func makeFeatureFlagCache(mobileKey: MobileKey, maxCachedUsers: Int = 5) -> FeatureFlagCaching { + var makeFeatureFlagCacheReceivedParameters: (mobileKey: MobileKey, maxCachedContexts: Int)? = nil + func makeFeatureFlagCache(mobileKey: MobileKey, maxCachedContexts: Int = 5) -> FeatureFlagCaching { makeFeatureFlagCacheCallCount += 1 - makeFeatureFlagCacheReceivedParameters = (mobileKey: mobileKey, maxCachedUsers: maxCachedUsers) + makeFeatureFlagCacheReceivedParameters = (mobileKey: mobileKey, maxCachedContexts: maxCachedContexts) makeFeatureFlagCacheCallback?() return makeFeatureFlagCacheReturnValue } @@ -28,8 +28,8 @@ final class ClientServiceMockFactory: ClientServiceCreating { return makeCacheConverterReturnValue } - func makeDarklyServiceProvider(config: LDConfig, user: LDUser) -> DarklyServiceProvider { - DarklyServiceMock(config: config, user: user) + func makeDarklyServiceProvider(config: LDConfig, context: LDContext) -> DarklyServiceProvider { + DarklyServiceMock(config: config, context: context) } var makeFlagSynchronizerCallCount = 0 @@ -75,12 +75,12 @@ final class ClientServiceMockFactory: ClientServiceCreating { } var makeStreamingProviderCallCount = 0 - var makeStreamingProviderReceivedArguments: (url: URL, - httpHeaders: [String: String], - connectMethod: String?, - connectBody: Data?, - handler: EventHandler, - delegate: RequestHeaderTransform?, + var makeStreamingProviderReceivedArguments: (url: URL, + httpHeaders: [String: String], + connectMethod: String?, + connectBody: Data?, + handler: EventHandler, + delegate: RequestHeaderTransform?, errorHandler: ConnectionErrorHandler?)? func makeStreamingProvider(url: URL, httpHeaders: [String: String], connectMethod: String, connectBody: Data?, handler: EventHandler, delegate: RequestHeaderTransform?, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider { makeStreamingProviderCallCount += 1 @@ -116,7 +116,7 @@ final class ClientServiceMockFactory: ClientServiceCreating { } return throttlingMock } - + func makeConnectionInformation() -> ConnectionInformation { ConnectionInformation(currentConnectionMode: .offline, lastConnectionFailureReason: .none) } diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift index 3e212454..32502f73 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift @@ -63,7 +63,7 @@ final class DarklyServiceMock: DarklyServiceProvider { static let trackEvents = true static let debugEventsUntilDate = Date().addingTimeInterval(30.0) static let reason: [String: LDValue] = ["kind": "OFF"] - + static func stubFeatureFlags(debugEventsUntilDate: Date? = Date().addingTimeInterval(30.0)) -> [LDFlagKey: FeatureFlag] { let flagKeys = FlagKeys.knownFlags let featureFlagTuples = flagKeys.map { flagKey in @@ -94,16 +94,16 @@ final class DarklyServiceMock: DarklyServiceProvider { } var config: LDConfig - var user: LDUser + var context: LDContext var diagnosticCache: DiagnosticCaching? = nil var activationBlocks = [(testBlock: HTTPStubsTestBlock, callback: ((URLRequest, HTTPStubsDescriptor, HTTPStubsResponse) -> Void))]() - init(config: LDConfig = LDConfig.stub, user: LDUser = LDUser.stub()) { + init(config: LDConfig = LDConfig.stub, context: LDContext = LDContext.stub()) { self.config = config - self.user = user + self.context = context } - + var stubbedFlagResponse: ServiceResponse? var getFeatureFlagsUseReportCalledValue = [Bool]() var getFeatureFlagsCallCount = 0 @@ -117,7 +117,7 @@ final class DarklyServiceMock: DarklyServiceProvider { func clearFlagResponseCache() { clearFlagResponseCacheCallCount += 1 } - + var createdEventSource: DarklyStreamingProviderMock? var createEventSourceCallCount = 0 var createEventSourceReceivedUseReport: Bool? @@ -132,7 +132,7 @@ final class DarklyServiceMock: DarklyServiceProvider { createdEventSource = mock return mock } - + var stubbedEventResponse: ServiceResponse? var publishEventDataCallCount = 0 var publishedEventData: Data? diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift index 359141ec..e933cad2 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift @@ -2,7 +2,6 @@ import Foundation extension EnvironmentReportingMock { struct Constants { - static let deviceModel = "deviceModelStub" static let deviceType = "deviceTypeStub" static let systemVersion = "systemVersionStub" static let systemName = "systemNameStub" diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift new file mode 100644 index 00000000..97ebb090 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift @@ -0,0 +1,50 @@ +import Foundation +@testable import LaunchDarkly + +extension LDContext { + struct StubConstants { + static let key: LDValue = "stub.context.key" + + static let name = "stub.context.name" + static let isAnonymous = false + + static let firstName: LDValue = "stub.context.firstName" + static let lastName: LDValue = "stub.context.lastName" + static let country: LDValue = "stub.context.country" + static let ipAddress: LDValue = "stub.context.ipAddress" + static let email: LDValue = "stub.context@email.com" + static let avatar: LDValue = "stub.context.avatar" + static let custom: [String: LDValue] = ["stub.context.custom.keyA": "stub.context.custom.valueA", + "stub.context.custom.keyB": true, + "stub.context.custom.keyC": 1027, + "stub.context.custom.keyD": 2.71828, + "stub.context.custom.keyE": [0, 1, 2], + "stub.context.custom.keyF": ["1": 1, "2": 2, "3": 3]] + } + + static func stub(key: String? = nil, + environmentReporter: EnvironmentReportingMock? = nil) -> LDContext { + var builder = LDContextBuilder(key: key ?? UUID().uuidString) + + builder.name(StubConstants.name) + builder.anonymous(StubConstants.isAnonymous) + + builder.trySetValue("firstName", StubConstants.firstName) + builder.trySetValue("lastName", StubConstants.lastName) + builder.trySetValue("country", StubConstants.country) + builder.trySetValue("ip", StubConstants.ipAddress) + builder.trySetValue("email", StubConstants.email) + builder.trySetValue("avatar", StubConstants.avatar) + + for (key, value) in StubConstants.custom { + builder.trySetValue(key, value) + } + + var context: LDContext? = nil + if case .success(let ctx) = builder.build() { + context = ctx + } + + return context! + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift index 943be88c..67023d96 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift @@ -4,7 +4,6 @@ import Foundation extension LDUser { struct StubConstants { static let key = "stub.user.key" - static let secondary = "stub.user.secondary" static let userKey = "userKey" static let name = "stub.user.name" static let firstName = "stub.user.firstName" @@ -44,8 +43,7 @@ extension LDUser { email: StubConstants.email, avatar: StubConstants.avatar, custom: StubConstants.custom(includeSystemValues: true), - isAnonymous: StubConstants.isAnonymous, - secondary: StubConstants.secondary) + isAnonymous: StubConstants.isAnonymous) return user } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/KindSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/KindSpec.swift new file mode 100644 index 00000000..611f51a4 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/KindSpec.swift @@ -0,0 +1,72 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class KindSpec: XCTestCase { + func testKindCorrectlyIdentifiesAsMulti() { + let options: [(Kind, Bool)] = [ + (.user, false), + (.multi, true), + (.custom("multi"), true), + (.custom("org"), false) + ] + + for (kind, isMulti) in options { + XCTAssertEqual(kind.isMulti(), isMulti) + } + } + + func testKindCorrectlyIdentifiesAsUser() { + let options: [(Kind, Bool)] = [ + (.user, true), + (.multi, false), + (.custom(""), true), + (.custom("user"), true), + (.custom("org"), false) + ] + + for (kind, isUser) in options { + XCTAssertEqual(kind.isUser(), isUser) + } + } + + func testKindBuildsFromStringCorrectly() { + XCTAssertNil(Kind("kind")) + XCTAssertNil(Kind("no spaces allowed")) + XCTAssertNil(Kind("#invalidcharactersarefun")) + + XCTAssertEqual(Kind(""), .user) + XCTAssertEqual(Kind("user"), .user) + XCTAssertEqual(Kind("User"), .custom("User")) + + XCTAssertEqual(Kind("multi"), .multi) + XCTAssertEqual(Kind("org"), .custom("org")) + } + + func testKindCanEncodeAndDecodeAppropriately() throws { + // I know it seems silly to have these test cases be arrays instead of + // simple strings. However, if I can please kindly direct your + // attention to https://github.com/apple/swift-corelibs-foundation/issues/4402 + // you will see that older versions had an issue encoding and decoding JSON + // fragments like simple strings. + // + // Using an array like this is a simple but effective workaround. + let testCases = [ + ("[\"user\"]", Kind("user"), true, false), + ("[\"multi\"]", Kind("multi"), false, true), + ("[\"org\"]", Kind("org"), false, false) + ] + + for (json, expectedKind, isUser, isMulti) in testCases { + let kindJson = Data(json.utf8) + let kinds = try JSONDecoder().decode([Kind].self, from: kindJson) + + XCTAssertEqual(expectedKind, kinds[0]) + XCTAssertEqual(isUser, kinds[0].isUser()) + XCTAssertEqual(isMulti, kinds[0].isMulti()) + + try XCTAssertEqual(kindJson, JSONEncoder().encode(kinds)) + } + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift new file mode 100644 index 00000000..f85b353d --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift @@ -0,0 +1,146 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class LDContextCodableSpec: XCTestCase { + func testUserFormatIsConvertedToSingleContextFormat() throws { + let testCases = [ + ("{\"key\": \"foo\"}", "{\"key\": \"foo\", \"kind\": \"user\"}"), + ("{\"key\" : \"foo\", \"name\" : \"bar\"}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"name\" : \"bar\"}"), + ("{\"key\" : \"foo\", \"custom\" : {\"a\" : \"b\"}}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"a\" : \"b\"}"), + ("{\"key\" : \"foo\", \"anonymous\" : true}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"anonymous\" : true}"), + ("{\"key\" : \"foo\"}", "{\"kind\" : \"user\", \"key\" : \"foo\"}"), + ("{\"key\" : \"foo\", \"ip\" : \"1\", \"privateAttributeNames\" : [\"ip\"]}", "{\"kind\" : \"user\", \"key\" : \"foo\", \"ip\" : \"1\", \"_meta\" : { \"privateAttributes\" : [\"ip\"]} }") + ] + + for (userJson, explicitFormat) in testCases { + let userContext = try JSONDecoder().decode(LDContext.self, from: Data(userJson.utf8)) + let explicitContext = try JSONDecoder().decode(LDContext.self, from: Data(explicitFormat.utf8)) + + XCTAssertEqual(userContext, explicitContext) + } + } + + func testUserCustomAttributesAreOverriddenByOldBuiltIns() throws { + let userJson = "{\"key\" : \"foo\", \"anonymous\" : true, \"secondary\": \"my secondary\", \"custom\": {\"anonymous\": false, \"secondary\": \"custom secondary\"}}" + // Secondary is not supported, so we should get the custom version for that. + let explicitFormat = "{\"kind\" : \"user\", \"key\" : \"foo\", \"anonymous\" : true, \"secondary\": \"custom secondary\"}" + + let userContext = try JSONDecoder().decode(LDContext.self, from: Data(userJson.utf8)) + let explicitContext = try JSONDecoder().decode(LDContext.self, from: Data(explicitFormat.utf8)) + + XCTAssertEqual(userContext, explicitContext) + } + + func testSingleContextKindsAreDecodedAndEncodedWithoutLossOfInformation() throws { + let testCases = [ + "{\"kind\":\"org\",\"key\":\"foo\"}", + "{\"kind\":\"user\",\"key\":\"foo\"}", + "{\"kind\":\"foo\",\"key\":\"bar\",\"anonymous\":true}", + "{\"kind\":\"foo\",\"key\":\"bar\",\"name\":\"Foo\",\"_meta\":{\"privateAttributes\":[\"a\"]}}", + "{\"kind\":\"foo\",\"key\":\"bar\",\"object\":{\"a\":\"b\"}}" + ] + + for json in testCases { + let context = try JSONDecoder().decode(LDContext.self, from: Data(json.utf8)) + let output = try JSONEncoder().encode(context) + let outputJson = String(data: output, encoding: .utf8) + + XCTAssertEqual(json, outputJson) + } + } + + func testAttributeRetractionWorksCorrectly() throws { + let json = """ + { + "kind":"foo", + "key":"bar", + "name":"Foo", + "a": "should be removed", + "b": { + "c": "should be removed", + "d": "should be retained" + }, + "/complex/attribute": "should be removed", + "_meta":{ + "privateAttributes":["a", "/b/c", "~1complex~1attribute"], + } + } + """ + + let context = try JSONDecoder().decode(LDContext.self, from: Data(json.utf8)) + let output = try JSONEncoder().encode(context) + let outputJson = String(data: output, encoding: .utf8) + + XCTAssertTrue(outputJson!.contains("should be retained")) + XCTAssertFalse(outputJson!.contains("should be removed")) + } + + func testGlobalAttributeRetractionWorksCorrectly() throws { + let json = """ + { + "kind":"foo", + "key":"bar", + "name":"Foo", + "a": "should be removed", + "b": { + "c": "should be removed", + "d": "should be retained" + }, + "_meta":{ + "privateAttributes":["a", "/b/c"], + } + } + """ + + let context = try JSONDecoder().decode(LDContext.self, from: Data(json.utf8)) + + let encodingConfig: [CodingUserInfoKey: Any] = + [ + LDContext.UserInfoKeys.globalPrivateAttributes: [Reference("a"), Reference("/b/c")] + ] + let encoder = JSONEncoder() + encoder.userInfo = encodingConfig + let output = try encoder.encode(context) + let outputJson = String(data: output, encoding: .utf8) + + XCTAssertTrue(outputJson!.contains("should be retained")) + XCTAssertFalse(outputJson!.contains("should be removed")) + } + + func testCanDecodeIntoMultiContextCorrectly() throws { + let json = """ + { + "kind": "multi", + "user": { + "key": "foo-key", + }, + "bar": { + "key": "bar-key" + }, + "baz": { + "key": "baz-key", + "anonymous": true + } + } + """ + let context = try JSONDecoder().decode(LDContext.self, from: Data(json.utf8)) + + let userBuilder = LDContextBuilder(key: "foo-key") + var barBuilder = LDContextBuilder(key: "bar-key") + barBuilder.kind("bar") + + var bazBuilder = LDContextBuilder(key: "baz-key") + bazBuilder.kind("baz") + bazBuilder.anonymous(true) + + var multiBuilder = LDMultiContextBuilder() + multiBuilder.addContext(try userBuilder.build().get()) + multiBuilder.addContext(try barBuilder.build().get()) + multiBuilder.addContext(try bazBuilder.build().get()) + let expectedContext = try multiBuilder.build().get() + + XCTAssertEqual(expectedContext, context) + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift new file mode 100644 index 00000000..953c8d4a --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift @@ -0,0 +1,290 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class LDContextSpec: XCTestCase { + func testBuildCanCreateSimpleContext() throws { + var builder = LDContextBuilder(key: "context-key") + builder.name("Name") + + let context = try builder.build().get() + XCTAssertFalse(context.isMulti()) + } + + func testBuilderWillNotAcceptKindOfTypeKind() { + var builder = LDContextBuilder(key: "context-key") + builder.kind("kind") + + guard case .failure(let error) = builder.build() + else { + XCTFail("Builder should not create context with kind 'kind'") + return + } + + XCTAssertEqual(error, ContextBuilderError.invalidKind) + } + + func testBuilderCanHandleMissingKind() throws { + var builder = LDContextBuilder(key: "key") + + var context = try builder.build().get() + XCTAssertTrue(context.kind.isUser()) + + builder.kind("") + context = try builder.build().get() + XCTAssertTrue(context.kind.isUser()) + } + + func testBuilderWillForceAnonymousToTrueForGeneratedKeys() throws { + var builder = LDContextBuilder() + builder.anonymous(false) + + let context = try builder.build().get() + XCTAssertFalse(context.fullyQualifiedKey().isEmpty) + + guard case let .bool(anonymous) = context.getValue(Reference("anonymous")) + else { + XCTFail("Anonymous could not be retrieved") + return + } + + XCTAssertTrue(anonymous) + } + + func testBuilderWillGenerateSameKeyForSameContextKind() throws { + var builder = LDContextBuilder() + let userContext1 = try builder.build().get() + let userContext2 = try builder.build().get() + + builder.kind("org") + + let orgContext1 = try builder.build().get() + let orgContext2 = try builder.build().get() + + builder.kind("user") + let userContext3 = try builder.build().get() + + // All user keys are the same + XCTAssertEqual(userContext1.fullyQualifiedKey(), userContext2.fullyQualifiedKey()) + XCTAssertEqual(userContext1.fullyQualifiedKey(), userContext3.fullyQualifiedKey()) + + // All org keys are the same + XCTAssertEqual(orgContext1.fullyQualifiedKey(), orgContext2.fullyQualifiedKey()) + + // But they aren't equal to each other + XCTAssertNotEqual(userContext1.fullyQualifiedKey(), orgContext1.fullyQualifiedKey()) + } + + func testSingleContextHasCorrectCanonicalKey() throws { + let tests: [(String, String, String)] = [ + ("key", "user", "key"), + ("key", "org", "org:key"), + ("hi:there", "user", "hi:there"), + ("hi:there", "org", "org:hi%3Athere") + ] + + for (key, kind, expectedKey) in tests { + var builder = LDContextBuilder(key: key) + builder.kind(kind) + + let context = try builder.build().get() + XCTAssertEqual(context.fullyQualifiedKey(), expectedKey) + } + } + + func testMultiContextHasCorrectCanonicalKey() throws { + let tests: [([(String, String)], String)] = [ + ([("key", "user")], "key"), + ([("userKey", "user"), ("orgKey", "org")], "org:orgKey:user:userKey"), + ([("some user", "user"), ("org:key", "org")], "org:org%3Akey:user:some%20user") + ] + + for (contextOptions, qualifiedKey) in tests { + var multibuilder = LDMultiContextBuilder() + + for (key, kind) in contextOptions { + var builder = LDContextBuilder(key: key) + builder.kind(kind) + + switch builder.build() { + case .success(let context): + multibuilder.addContext(context) + case .failure(let error): + XCTFail(error.localizedDescription) + } + } + + let context = try multibuilder.build().get() + XCTAssertEqual(context.fullyQualifiedKey(), qualifiedKey) + } + } + + func testMultikindBuilderRequiresContext() throws { + let multiBuilder = LDMultiContextBuilder() + switch multiBuilder.build() { + case .success: + XCTFail("Multibuilder should have failed to build.") + case .failure(let error): + XCTAssertEqual(error, .emptyMultiKind) + } + } + + func testMultikindCannotContainAnotherMultiKind() throws { + var multiBuilder = LDMultiContextBuilder() + + var builder = LDContextBuilder(key: "key") + multiBuilder.addContext(try builder.build().get()) + + builder.key("orgKey") + builder.kind("org") + multiBuilder.addContext(try builder.build().get()) + + let multiContext = try multiBuilder.build().get() + + multiBuilder.addContext(multiContext) + + switch multiBuilder.build() { + case .success: + XCTFail("Multibuilder should have failed to build with a multi-context.") + case .failure(let error): + XCTAssertEqual(error, .nestedMultiKind) + } + } + + func testMultikindBuilderFailsWithDuplicateContexts() throws { + var multiBuilder = LDMultiContextBuilder() + + multiBuilder.addContext(try LDContextBuilder(key: "key").build().get()) + multiBuilder.addContext(try LDContextBuilder(key: "second").build().get()) + + switch multiBuilder.build() { + case .success: + XCTFail("Multibuilder should have failed to build.") + case .failure(let error): + XCTAssertEqual(error, .duplicateKinds) + } + } + + func testCanSetCustomPropertiesByType() throws { + var builder = LDContextBuilder(key: "key") + builder.kind("user") + builder.trySetValue("loves-swift", true) + builder.trySetValue("pi", 3.1459) + builder.trySetValue("answer-to-life", 42) + builder.trySetValue("company", "LaunchDarkly") + + let context = try builder.build().get() + XCTAssertEqual(.bool(true), context.attributes["loves-swift"]) + XCTAssertEqual(.number(3.1459), context.attributes["pi"]) + XCTAssertEqual(.number(42), context.attributes["answer-to-life"]) + XCTAssertEqual(.string("LaunchDarkly"), context.attributes["company"]) + } + + func testCanSetAndRemovePrivateAttributes() throws { + var builder = LDContextBuilder(key: "key") + + XCTAssertTrue(try builder.build().get().privateAttributes.isEmpty) + + builder.addPrivateAttribute(Reference("name")) + XCTAssertTrue(try builder.build().get().privateAttributes.first == Reference("name")) + + builder.removePrivateAttribute(Reference("name")) + XCTAssertTrue(try builder.build().get().privateAttributes.isEmpty) + + // Removing one should remove them all + builder.addPrivateAttribute(Reference("name")) + builder.addPrivateAttribute(Reference("name")) + builder.removePrivateAttribute(Reference("name")) + XCTAssertTrue(try builder.build().get().privateAttributes.isEmpty) + } + + func testTrySetValueHandlesInvalidValues() { + let tests: [(String, LDValue, Bool)] = [ + ("", .bool(true), false), + ("kind", .bool(true), false), + ("kind", .string("user"), true) + ] + + for (attribute, value, expected) in tests { + var builder = LDContextBuilder(key: "key") + let result = builder.trySetValue(attribute, value) + XCTAssertEqual(result, expected) + } + } + + func testContextCanGetValue() throws { + let tests: [(String, LDValue?)] = [ + // Basic simple attribute retrievals + ("kind", .string("org")), + ("key", .string("my-key")), + ("name", .string("my-name")), + ("anonymous", .bool(true)), + ("attr", .string("my-attr")), + ("/starts-with-slash", .string("love that prefix")), + ("/crazy~0name", .string("still works")), + ("/other", nil), + // Invalid reference retrieval + ("/", nil), + ("", nil), + ("/a//b", nil), + // Hidden meta attributes + ("privateAttributes", nil), + // Can index arrays and objects + ("/my-map/array", .array([.string("first"), .string("second")])), + ("/my-map/1", .bool(true)), + ("my-map/missing", nil), + ("/starts-with-slash/1", nil) + ] + + let array: [LDValue] = [.string("first"), .string("second")] + let map: [String: LDValue] = ["array": .array(array), "1": .bool(true)] + + for (input, expectedValue) in tests { + var builder = LDContextBuilder(key: "my-key") + builder.kind("org") + builder.name("my-name") + builder.anonymous(true) + builder.trySetValue("attr", .string("my-attr")) + builder.trySetValue("starts-with-slash", .string("love that prefix")) + builder.trySetValue("crazy~name", .string("still works")) + builder.trySetValue("my-map", .object(map)) + + let context = try builder.build().get() + + let reference = Reference(input) + + XCTAssertEqual(expectedValue, context.getValue(reference)) + } + } + + func testMultiContextCanGetValue() throws { + var multibuilder = LDMultiContextBuilder() + var builder = LDContextBuilder(key: "user") + + multibuilder.addContext(try builder.build().get()) + + builder.key("org") + builder.kind("org") + builder.name("my-name") + builder.anonymous(true) + builder.trySetValue("attr", .string("my-attr")) + + multibuilder.addContext(try builder.build().get()) + + let context = try multibuilder.build().get() + + let tests: [(String, LDValue?)] = [ + ("kind", LDValue.string("multi")), + ("key", nil), + ("name", nil), + ("anonymous", nil), + ("attr", nil) + ] + + for (input, expectedValue) in tests { + let reference = Reference(input) + XCTAssertEqual(context.getValue(reference), expectedValue) + } + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/ReferenceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/ReferenceSpec.swift new file mode 100644 index 00000000..0cded180 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/ReferenceSpec.swift @@ -0,0 +1,83 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class ReferenceSpec: XCTestCase { + func testFailsWithCorrectError() { + let tests: [(String, ReferenceError)] = [ + ("", .empty), + ("/", .empty), + ("//", .doubleSlash), + ("/a//b", .doubleSlash), + ("/a/b/", .doubleSlash), + ("/~3", .invalidEscapeSequence), + ("/testing~something", .invalidEscapeSequence), + ("/m~~0", .invalidEscapeSequence), + ("/a~", .invalidEscapeSequence) + ] + + for (path, error) in tests { + let reference = Reference(path) + XCTAssertTrue(!reference.isValid()) + XCTAssertEqual(reference.getError(), error) + } + } + + func testWithoutLeadingSlashes() { + let tests = ["key", "kind", "name", "name/with/slashes", "name~0~1with-what-looks-like-escape-sequences"] + + for test in tests { + let ref = Reference(test) + XCTAssertTrue(ref.isValid()) + XCTAssertEqual(1, ref.depth()) + XCTAssertEqual(test, ref.component(0)) + } + } + + func testWithLeadingSlashes() { + let tests = [ + ("/key", "key"), + ("/kind", "kind"), + ("/name", "name"), + ("/custom", "custom") + ] + + for (ref, expected) in tests { + let ref = Reference(ref) + XCTAssertTrue(ref.isValid()) + XCTAssertEqual(1, ref.depth()) + XCTAssertEqual(expected, ref.component(0)) + } + } + + func testHandlesSubcomponents() { + let tests: [(String, Int, Int, String)] = [ + ("/a/b", 2, 0, "a"), + ("/a/b", 2, 1, "b"), + ("/a~1b/c", 2, 0, "a/b"), + ("/a~1b/c", 2, 1, "c"), + ("/a/10/20/30x", 4, 1, "10"), + ("/a/10/20/30x", 4, 2, "20"), + ("/a/10/20/30x", 4, 3, "30x") + ] + + for (input, expectedLength, index, expectedName) in tests { + let reference = Reference(input) + + XCTAssertEqual(expectedLength, reference.depth()) + XCTAssertEqual(expectedName, reference.component(index)) + } + } + + func testCanHandleInvalidIndexRequests() { + let reference = Reference("/a/b/c") + + XCTAssertTrue(reference.isValid()) + XCTAssertNotNil(reference.component(0)) + XCTAssertNotNil(reference.component(1)) + XCTAssertNotNil(reference.component(2)) + + XCTAssertNil(reference.component(3)) + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift index 005e882a..2ebb4158 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift @@ -178,14 +178,13 @@ final class DiagnosticEventSpec: QuickSpec { customConfig.connectionTimeout = 30.0 customConfig.eventFlushInterval = 60.0 customConfig.streamingMode = .polling - customConfig.allUserAttributesPrivate = true + customConfig.allContextAttributesPrivate = true customConfig.flagPollingInterval = 360.0 customConfig.backgroundFlagPollingInterval = 1_800.0 - customConfig.inlineUserInEvents = true customConfig.useReport = true customConfig.enableBackgroundUpdates = true customConfig.evaluationReasons = true - customConfig.maxCachedUsers = -2 + customConfig.maxCachedContexts = -2 try! customConfig.setSecondaryMobileKeys(["test": "foobar1", "debug": "foobar2"]) customConfig.diagnosticRecordingInterval = 600.0 customConfig.wrapperName = "ReactNative" @@ -212,11 +211,10 @@ final class DiagnosticEventSpec: QuickSpec { expect(diagnosticConfig.allAttributesPrivate) == false expect(diagnosticConfig.pollingIntervalMillis) == 300_000 expect(diagnosticConfig.backgroundPollingIntervalMillis) == 3_600_000 - expect(diagnosticConfig.inlineUsersInEvents) == false expect(diagnosticConfig.useReport) == false expect(diagnosticConfig.backgroundPollingDisabled) == true expect(diagnosticConfig.evaluationReasonsRequested) == false - expect(diagnosticConfig.maxCachedUsers) == 5 + expect(diagnosticConfig.maxCachedContexts) == 5 expect(diagnosticConfig.mobileKeyCount) == 1 expect(diagnosticConfig.diagnosticRecordingIntervalMillis) == 900_000 expect(diagnosticConfig.customHeaders) == false @@ -235,12 +233,11 @@ final class DiagnosticEventSpec: QuickSpec { expect(diagnosticConfig.allAttributesPrivate) == true expect(diagnosticConfig.pollingIntervalMillis) == 360_000 expect(diagnosticConfig.backgroundPollingIntervalMillis) == 1_800_000 - expect(diagnosticConfig.inlineUsersInEvents) == true expect(diagnosticConfig.useReport) == true expect(diagnosticConfig.backgroundPollingDisabled) == false expect(diagnosticConfig.evaluationReasonsRequested) == true // All negative values become -1 for consistency - expect(diagnosticConfig.maxCachedUsers) == -1 + expect(diagnosticConfig.maxCachedContexts) == -1 expect(diagnosticConfig.mobileKeyCount) == 3 expect(diagnosticConfig.diagnosticRecordingIntervalMillis) == 600_000 expect(diagnosticConfig.customHeaders) == false @@ -270,8 +267,7 @@ final class DiagnosticEventSpec: QuickSpec { } it("encodes correct values to keys") { encodesToObject(diagnosticConfig) { decoded in - expect(decoded.count) == 19 - expect(decoded["autoAliasingOptOut"]) == .bool(diagnosticConfig.autoAliasingOptOut) + expect(decoded.count) == 17 expect(decoded["customBaseURI"]) == .bool(diagnosticConfig.customBaseURI) expect(decoded["customEventsURI"]) == .bool(diagnosticConfig.customEventsURI) expect(decoded["customStreamURI"]) == .bool(diagnosticConfig.customStreamURI) @@ -282,11 +278,10 @@ final class DiagnosticEventSpec: QuickSpec { expect(decoded["allAttributesPrivate"]) == .bool(diagnosticConfig.allAttributesPrivate) expect(decoded["pollingIntervalMillis"]) == .number(Double(diagnosticConfig.pollingIntervalMillis)) expect(decoded["backgroundPollingIntervalMillis"]) == .number(Double(diagnosticConfig.backgroundPollingIntervalMillis)) - expect(decoded["inlineUsersInEvents"]) == .bool(diagnosticConfig.inlineUsersInEvents) expect(decoded["useReport"]) == .bool(diagnosticConfig.useReport) expect(decoded["backgroundPollingDisabled"]) == .bool(diagnosticConfig.backgroundPollingDisabled) expect(decoded["evaluationReasonsRequested"]) == .bool(diagnosticConfig.evaluationReasonsRequested) - expect(decoded["maxCachedUsers"]) == .number(Double(diagnosticConfig.maxCachedUsers)) + expect(decoded["maxCachedContexts"]) == .number(Double(diagnosticConfig.maxCachedContexts)) expect(decoded["mobileKeyCount"]) == .number(Double(diagnosticConfig.mobileKeyCount)) expect(decoded["diagnosticRecordingIntervalMillis"]) == .number(Double(diagnosticConfig.diagnosticRecordingIntervalMillis)) } @@ -303,11 +298,10 @@ final class DiagnosticEventSpec: QuickSpec { expect(decoded?.allAttributesPrivate) == diagnosticConfig.allAttributesPrivate expect(decoded?.pollingIntervalMillis) == diagnosticConfig.pollingIntervalMillis expect(decoded?.backgroundPollingIntervalMillis) == diagnosticConfig.backgroundPollingIntervalMillis - expect(decoded?.inlineUsersInEvents) == diagnosticConfig.inlineUsersInEvents expect(decoded?.useReport) == diagnosticConfig.useReport expect(decoded?.backgroundPollingDisabled) == diagnosticConfig.backgroundPollingDisabled expect(decoded?.evaluationReasonsRequested) == diagnosticConfig.evaluationReasonsRequested - expect(decoded?.maxCachedUsers) == diagnosticConfig.maxCachedUsers + expect(decoded?.maxCachedContexts) == diagnosticConfig.maxCachedContexts expect(decoded?.mobileKeyCount) == diagnosticConfig.mobileKeyCount expect(decoded?.diagnosticRecordingIntervalMillis) == diagnosticConfig.diagnosticRecordingIntervalMillis } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index 4bf2efd9..e61700cf 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -4,25 +4,14 @@ import XCTest @testable import LaunchDarkly final class EventSpec: XCTestCase { - func testAliasEventInit() { - let testDate = Date() - let event = AliasEvent(key: "abc", previousKey: "def", contextKind: "user", previousContextKind: "anonymousUser", creationDate: testDate) - XCTAssertEqual(event.kind, .alias) - XCTAssertEqual(event.key, "abc") - XCTAssertEqual(event.previousKey, "def") - XCTAssertEqual(event.contextKind, "user") - XCTAssertEqual(event.previousContextKind, "anonymousUser") - XCTAssertEqual(event.creationDate, testDate) - } - func testFeatureEventInit() { let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - let user = LDUser.stub() + let context = LDContext.stub() let testDate = Date() - let event = FeatureEvent(key: "abc", user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: true, isDebug: false, creationDate: testDate) + let event = FeatureEvent(key: "abc", context: context, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: true, isDebug: false, creationDate: testDate) XCTAssertEqual(event.kind, Event.Kind.feature) XCTAssertEqual(event.key, "abc") - XCTAssertEqual(event.user, user) + XCTAssertEqual(event.context, context) XCTAssertEqual(event.value, true) XCTAssertEqual(event.defaultValue, false) XCTAssertEqual(event.featureFlag, featureFlag) @@ -32,12 +21,12 @@ final class EventSpec: XCTestCase { func testDebugEventInit() { let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - let user = LDUser.stub() + let context = LDContext.stub() let testDate = Date() - let event = FeatureEvent(key: "abc", user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: true, creationDate: testDate) + let event = FeatureEvent(key: "abc", context: context, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: true, creationDate: testDate) XCTAssertEqual(event.kind, Event.Kind.debug) XCTAssertEqual(event.key, "abc") - XCTAssertEqual(event.user, user) + XCTAssertEqual(event.context, context) XCTAssertEqual(event.value, true) XCTAssertEqual(event.defaultValue, false) XCTAssertEqual(event.featureFlag, featureFlag) @@ -46,12 +35,12 @@ final class EventSpec: XCTestCase { } func testCustomEventInit() { - let user = LDUser.stub() + let context = LDContext.stub() let testDate = Date() - let event = CustomEvent(key: "abc", user: user, data: ["abc": 123], metricValue: 5.0, creationDate: testDate) + let event = CustomEvent(key: "abc", context: context, data: ["abc": 123], metricValue: 5.0, creationDate: testDate) XCTAssertEqual(event.kind, Event.Kind.custom) XCTAssertEqual(event.key, "abc") - XCTAssertEqual(event.user, user) + XCTAssertEqual(event.context, context) XCTAssertEqual(event.data, ["abc": 123]) XCTAssertEqual(event.metricValue, 5.0) XCTAssertEqual(event.creationDate, testDate) @@ -59,10 +48,10 @@ final class EventSpec: XCTestCase { func testIdentifyEventInit() { let testDate = Date() - let user = LDUser.stub() - let event = IdentifyEvent(user: user, creationDate: testDate) + let context = LDContext.stub() + let event = IdentifyEvent(context: context, creationDate: testDate) XCTAssertEqual(event.kind, Event.Kind.identify) - XCTAssertEqual(event.user, user) + XCTAssertEqual(event.context, context) XCTAssertEqual(event.creationDate, testDate) } @@ -76,65 +65,51 @@ final class EventSpec: XCTestCase { XCTAssertEqual(event.flagRequestTracker.flagCounters, flagRequestTracker.flagCounters) } - func testAliasEventEncoding() { - let event = AliasEvent(key: "abc", previousKey: "def", contextKind: "user", previousContextKind: "anonymousUser") - encodesToObject(event) { dict in - XCTAssertEqual(dict.count, 6) - XCTAssertEqual(dict["kind"], "alias") - XCTAssertEqual(dict["key"], "abc") - XCTAssertEqual(dict["previousKey"], "def") - XCTAssertEqual(dict["contextKind"], "user") - XCTAssertEqual(dict["previousContextKind"], "anonymousUser") - XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) - } - } - func testCustomEventEncodingDataAndMetric() { - let user = LDUser.stub() - let event = CustomEvent(key: "event-key", user: user, data: ["abc", 12], metricValue: 0.5) + let context = LDContext.stub() + let event = CustomEvent(key: "event-key", context: context, data: ["abc", 12], metricValue: 0.5) encodesToObject(event) { dict in XCTAssertEqual(dict.count, 6) XCTAssertEqual(dict["kind"], "custom") XCTAssertEqual(dict["key"], "event-key") XCTAssertEqual(dict["data"], ["abc", 12]) XCTAssertEqual(dict["metricValue"], 0.5) - XCTAssertEqual(dict["userKey"], .string(user.key)) + XCTAssertEqual(dict["contextKeys"], .object(["user": .string(context.fullyQualifiedKey())])) XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } - func testCustomEventEncodingAnonUser() { - let anonUser = LDUser() - let event = CustomEvent(key: "event-key", user: anonUser, data: ["key": "val"]) + func testCustomEventEncodingAnonContext() { + let context = LDContext.stub() + let event = CustomEvent(key: "event-key", context: context, data: ["key": "val"]) encodesToObject(event) { dict in - XCTAssertEqual(dict.count, 6) + XCTAssertEqual(dict.count, 5) XCTAssertEqual(dict["kind"], "custom") XCTAssertEqual(dict["key"], "event-key") XCTAssertEqual(dict["data"], ["key": "val"]) - XCTAssertEqual(dict["userKey"], .string(anonUser.key)) - XCTAssertEqual(dict["contextKind"], "anonymousUser") + XCTAssertEqual(dict["contextKeys"], .object(["user": .string(context.fullyQualifiedKey())])) XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } func testCustomEventEncodingInlining() { - let user = LDUser.stub() - let event = CustomEvent(key: "event-key", user: user, data: nil, metricValue: 2.5) - encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineUserInEvents: true]) { dict in + let context = LDContext.stub() + let event = CustomEvent(key: "event-key", context: context, data: nil, metricValue: 2.5) + encodesToObject(event) { dict in XCTAssertEqual(dict.count, 5) XCTAssertEqual(dict["kind"], "custom") XCTAssertEqual(dict["key"], "event-key") XCTAssertEqual(dict["metricValue"], 2.5) - XCTAssertEqual(dict["user"], encodeToLDValue(user)) + XCTAssertEqual(dict["contextKeys"], .object(["user": .string(context.fullyQualifiedKey())])) XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } func testFeatureEventEncodingNoReasonByDefault() { - let user = LDUser.stub() + let context = LDContext.stub() let featureFlag = FeatureFlag(flagKey: "flag-key", variation: 2, flagVersion: 3, reason: ["kind": "OFF"]) [false, true].forEach { isDebug in - let event = FeatureEvent(key: "event-key", user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: isDebug) + let event = FeatureEvent(key: "event-key", context: context, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: isDebug) encodesToObject(event) { dict in XCTAssertEqual(dict.count, 8) XCTAssertEqual(dict["kind"], isDebug ? "debug" : "feature") @@ -144,9 +119,9 @@ final class EventSpec: XCTestCase { XCTAssertEqual(dict["variation"], 2) XCTAssertEqual(dict["version"], 3) if isDebug { - XCTAssertEqual(dict["user"], encodeToLDValue(user)) + XCTAssertEqual(dict["context"], encodeToLDValue(context)) } else { - XCTAssertEqual(dict["userKey"], .string(user.key)) + XCTAssertEqual(dict["contextKeys"], .object(["user": .string(context.fullyQualifiedKey())])) } XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } @@ -154,10 +129,10 @@ final class EventSpec: XCTestCase { } func testFeatureEventEncodingIncludeReason() { - let user = LDUser.stub() + let context = LDContext.stub() let featureFlag = FeatureFlag(flagKey: "flag-key", variation: 2, version: 2, flagVersion: 3, reason: ["kind": "OFF"]) [false, true].forEach { isDebug in - let event = FeatureEvent(key: "event-key", user: user, value: 3, defaultValue: 4, featureFlag: featureFlag, includeReason: true, isDebug: isDebug) + let event = FeatureEvent(key: "event-key", context: context, value: 3, defaultValue: 4, featureFlag: featureFlag, includeReason: true, isDebug: isDebug) encodesToObject(event) { dict in XCTAssertEqual(dict.count, 9) XCTAssertEqual(dict["kind"], isDebug ? "debug" : "feature") @@ -168,9 +143,9 @@ final class EventSpec: XCTestCase { XCTAssertEqual(dict["version"], 3) XCTAssertEqual(dict["reason"], ["kind": "OFF"]) if isDebug { - XCTAssertEqual(dict["user"], encodeToLDValue(user)) + XCTAssertEqual(dict["context"], encodeToLDValue(context)) } else { - XCTAssertEqual(dict["userKey"], .string(user.key)) + XCTAssertEqual(dict["contextKeys"], .object(["user": .string(context.fullyQualifiedKey())])) } XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } @@ -178,10 +153,10 @@ final class EventSpec: XCTestCase { } func testFeatureEventEncodingTrackReason() { - let user = LDUser.stub() + let context = LDContext.stub() let featureFlag = FeatureFlag(flagKey: "flag-key", reason: ["kind": "OFF"], trackReason: true) [false, true].forEach { isDebug in - let event = FeatureEvent(key: "event-key", user: user, value: nil, defaultValue: nil, featureFlag: featureFlag, includeReason: false, isDebug: isDebug) + let event = FeatureEvent(key: "event-key", context: context, value: nil, defaultValue: nil, featureFlag: featureFlag, includeReason: false, isDebug: isDebug) encodesToObject(event) { dict in XCTAssertEqual(dict.count, 7) XCTAssertEqual(dict["kind"], isDebug ? "debug" : "feature") @@ -190,9 +165,9 @@ final class EventSpec: XCTestCase { XCTAssertEqual(dict["default"], .null) XCTAssertEqual(dict["reason"], ["kind": "OFF"]) if isDebug { - XCTAssertEqual(dict["user"], encodeToLDValue(user)) + XCTAssertEqual(dict["context"], encodeToLDValue(context)) } else { - XCTAssertEqual(dict["userKey"], .string(user.key)) + XCTAssertEqual(dict["contextKeys"], .object(["user": .string(context.fullyQualifiedKey())])) } XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } @@ -200,62 +175,57 @@ final class EventSpec: XCTestCase { } func testFeatureEventEncodingAnonContextKind() { - let user = LDUser() + let context = LDContext.stub() [false, true].forEach { isDebug in - let event = FeatureEvent(key: "event-key", user: user, value: true, defaultValue: false, featureFlag: nil, includeReason: true, isDebug: isDebug) + let event = FeatureEvent(key: "event-key", context: context, value: true, defaultValue: false, featureFlag: nil, includeReason: true, isDebug: isDebug) encodesToObject(event) { dict in - XCTAssertEqual(dict.count, isDebug ? 6 : 7) + XCTAssertEqual(dict.count, 6) XCTAssertEqual(dict["kind"], isDebug ? "debug" : "feature") XCTAssertEqual(dict["key"], "event-key") XCTAssertEqual(dict["value"], true) XCTAssertEqual(dict["default"], false) if isDebug { - XCTAssertEqual(dict["user"], encodeToLDValue(user)) + XCTAssertEqual(dict["context"], encodeToLDValue(context)) } else { - XCTAssertEqual(dict["userKey"], .string(user.key)) - XCTAssertEqual(dict["contextKind"], "anonymousUser") + XCTAssertEqual(dict["contextKeys"], .object(["user": .string(context.fullyQualifiedKey())])) } XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) } } } - func testFeatureEventEncodingInlinesUserForDebugOrConfig() { - let user = LDUser.stub() + func testFeatureEventEncodingInlinesContextForDebug() { + let context = LDContext.stub() let featureFlag = FeatureFlag(flagKey: "flag-key", version: 3) - let featureEvent = FeatureEvent(key: "event-key", user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: false) - let debugEvent = FeatureEvent(key: "event-key", user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: true) - let encodedFeature = encodeToLDValue(featureEvent, userInfo: [Event.UserInfoKeys.inlineUserInEvents: true]) - let encodedDebug = encodeToLDValue(debugEvent, userInfo: [Event.UserInfoKeys.inlineUserInEvents: false]) - [encodedFeature, encodedDebug].forEach { valueIsObject($0) { dict in + let debugEvent = FeatureEvent(key: "event-key", context: context, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: true) + let encodedDebug = encodeToLDValue(debugEvent) + [encodedDebug].forEach { valueIsObject($0) { dict in XCTAssertEqual(dict.count, 7) XCTAssertEqual(dict["key"], "event-key") XCTAssertEqual(dict["value"], true) XCTAssertEqual(dict["default"], false) XCTAssertEqual(dict["version"], 3) - XCTAssertEqual(dict["user"], encodeToLDValue(user)) + XCTAssertEqual(dict["context"], encodeToLDValue(context)) }} } - func testIdentifyEventEncoding() { - let user = LDUser.stub() - for inlineUser in [true, false] { - let event = IdentifyEvent(user: user) - encodesToObject(event, userInfo: [Event.UserInfoKeys.inlineUserInEvents: inlineUser]) { dict in - XCTAssertEqual(dict.count, 4) + func testIdentifyEventEncoding() throws { + let context = LDContext.stub() + let event = IdentifyEvent(context: context) + encodesToObject(event) { dict in + XCTAssertEqual(dict.count, 4) XCTAssertEqual(dict["kind"], "identify") - XCTAssertEqual(dict["key"], .string(user.key)) - XCTAssertEqual(dict["user"], encodeToLDValue(user)) + XCTAssertEqual(dict["key"], .string(context.fullyQualifiedKey())) + XCTAssertEqual(dict["context"], encodeToLDValue(context)) XCTAssertEqual(dict["creationDate"], .number(Double(event.creationDate.millisSince1970))) - } } } func testSummaryEventEncoding() { let flag = FeatureFlag(flagKey: "bool-flag", variation: 1, version: 5, flagVersion: 2) var flagRequestTracker = FlagRequestTracker() - flagRequestTracker.trackRequest(flagKey: "bool-flag", reportedValue: false, featureFlag: flag, defaultValue: true) - flagRequestTracker.trackRequest(flagKey: "bool-flag", reportedValue: false, featureFlag: flag, defaultValue: true) + flagRequestTracker.trackRequest(flagKey: "bool-flag", reportedValue: false, featureFlag: flag, defaultValue: true, context: LDContext.stub()) + flagRequestTracker.trackRequest(flagKey: "bool-flag", reportedValue: false, featureFlag: flag, defaultValue: true, context: LDContext.stub()) let event = SummaryEvent(flagRequestTracker: flagRequestTracker, endDate: Date()) encodesToObject(event) { dict in XCTAssertEqual(dict.count, 4) @@ -265,8 +235,8 @@ final class EventSpec: XCTestCase { valueIsObject(dict["features"]) { features in XCTAssertEqual(features.count, 1) let counter = FlagCounter() - counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true) - counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true) + counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true, context: LDContext.stub()) + counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true, context: LDContext.stub()) XCTAssertEqual(features["bool-flag"], encodeToLDValue(counter)) } } @@ -275,24 +245,23 @@ final class EventSpec: XCTestCase { extension Event: Equatable { public static func == (_ lhs: Event, _ rhs: Event) -> Bool { - let config = [LDUser.UserInfoKeys.includePrivateAttributes: true, Event.UserInfoKeys.inlineUserInEvents: true] + let config = [LDContext.UserInfoKeys.includePrivateAttributes: true] return encodeToLDValue(lhs, userInfo: config) == encodeToLDValue(rhs, userInfo: config) } } extension Event { - static func stub(_ eventKind: Kind, with user: LDUser) -> Event { + static func stub(_ eventKind: Kind, with context: LDContext) -> Event { switch eventKind { case .feature: let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - return FeatureEvent(key: UUID().uuidString, user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: false) + return FeatureEvent(key: UUID().uuidString, context: context, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: false) case .debug: let featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool) - return FeatureEvent(key: UUID().uuidString, user: user, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: true) - case .identify: return IdentifyEvent(user: user) - case .custom: return CustomEvent(key: UUID().uuidString, user: user, data: ["custom": .string(UUID().uuidString)]) + return FeatureEvent(key: UUID().uuidString, context: context, value: true, defaultValue: false, featureFlag: featureFlag, includeReason: false, isDebug: true) + case .identify: return IdentifyEvent(context: context) + case .custom: return CustomEvent(key: UUID().uuidString, context: context, data: ["custom": .string(UUID().uuidString)]) case .summary: return SummaryEvent(flagRequestTracker: FlagRequestTracker.stub()) - case .alias: return AliasEvent(key: UUID().uuidString, previousKey: UUID().uuidString, contextKind: "anonymousUser", previousContextKind: "anonymousUser") } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift index 3186dc39..b235bbbe 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagRequestTracking/FlagCounterSpec.swift @@ -16,7 +16,7 @@ final class FlagCounterSpec: XCTestCase { func testTrackRequestInitialKnown() { let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 2, flagVersion: 3) let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue, context: LDContext.stub()) XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) XCTAssertEqual(flagCounter.flagValueCounters.count, 1) let counter = flagCounter.flagValueCounters.first! @@ -30,8 +30,8 @@ final class FlagCounterSpec: XCTestCase { let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 5, flagVersion: 3) let secondFeatureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 7, flagVersion: 3) let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: "e") - flagCounter.trackRequest(reportedValue: "b", featureFlag: secondFeatureFlag, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: "e", context: LDContext.stub()) + flagCounter.trackRequest(reportedValue: "b", featureFlag: secondFeatureFlag, defaultValue: testDefaultValue, context: LDContext.stub()) XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) XCTAssertEqual(flagCounter.flagValueCounters.count, 1) let counter = flagCounter.flagValueCounters.first! @@ -45,8 +45,8 @@ final class FlagCounterSpec: XCTestCase { let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 10, flagVersion: 5) let secondFeatureFlag = FeatureFlag(flagKey: "test-key", variation: 3, version: 10, flagVersion: 5) let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue) - flagCounter.trackRequest(reportedValue: testValue, featureFlag: secondFeatureFlag, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue, context: LDContext.stub()) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: secondFeatureFlag, defaultValue: testDefaultValue, context: LDContext.stub()) XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) XCTAssertEqual(flagCounter.flagValueCounters.count, 2) let counter1 = flagCounter.flagValueCounters.first { key, _ in key.variation == 2 }! @@ -63,8 +63,8 @@ final class FlagCounterSpec: XCTestCase { let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 10, flagVersion: 3) let secondFeatureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 10, flagVersion: 5) let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue) - flagCounter.trackRequest(reportedValue: testValue, featureFlag: secondFeatureFlag, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue, context: LDContext.stub()) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: secondFeatureFlag, defaultValue: testDefaultValue, context: LDContext.stub()) XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) XCTAssertEqual(flagCounter.flagValueCounters.count, 2) let counter1 = flagCounter.flagValueCounters.first { key, _ in key.version == 3 }! @@ -81,8 +81,8 @@ final class FlagCounterSpec: XCTestCase { let featureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 10) let secondFeatureFlag = FeatureFlag(flagKey: "test-key", variation: 2, version: 5, flagVersion: 10) let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue) - flagCounter.trackRequest(reportedValue: testValue, featureFlag: secondFeatureFlag, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: featureFlag, defaultValue: testDefaultValue, context: LDContext.stub()) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: secondFeatureFlag, defaultValue: testDefaultValue, context: LDContext.stub()) XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) XCTAssertEqual(flagCounter.flagValueCounters.count, 1) let counter = flagCounter.flagValueCounters.first! @@ -94,7 +94,7 @@ final class FlagCounterSpec: XCTestCase { func testTrackRequestInitialUnknown() { let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: testValue, featureFlag: nil, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: nil, defaultValue: testDefaultValue, context: LDContext.stub()) XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) XCTAssertEqual(flagCounter.flagValueCounters.count, 1) let counter = flagCounter.flagValueCounters.first! @@ -106,8 +106,8 @@ final class FlagCounterSpec: XCTestCase { func testTrackRequestSecondUnknown() { let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: testValue, featureFlag: nil, defaultValue: testDefaultValue) - flagCounter.trackRequest(reportedValue: testValue, featureFlag: nil, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: nil, defaultValue: testDefaultValue, context: LDContext.stub()) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: nil, defaultValue: testDefaultValue, context: LDContext.stub()) XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) XCTAssertEqual(flagCounter.flagValueCounters.count, 1) let counter = flagCounter.flagValueCounters.first! @@ -121,8 +121,8 @@ final class FlagCounterSpec: XCTestCase { let unknownFlag1 = FeatureFlag(flagKey: "unused", variation: 1) let unknownFlag2 = FeatureFlag(flagKey: "unused", variation: 2) let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: testValue, featureFlag: unknownFlag1, defaultValue: testDefaultValue) - flagCounter.trackRequest(reportedValue: testValue, featureFlag: unknownFlag2, defaultValue: testDefaultValue) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: unknownFlag1, defaultValue: testDefaultValue, context: LDContext.stub()) + flagCounter.trackRequest(reportedValue: testValue, featureFlag: unknownFlag2, defaultValue: testDefaultValue, context: LDContext.stub()) XCTAssertEqual(flagCounter.defaultValue, testDefaultValue) XCTAssertEqual(flagCounter.flagValueCounters.count, 2) let counter1 = flagCounter.flagValueCounters.first { key, _ in key.variation == 1 }! @@ -140,11 +140,12 @@ final class FlagCounterSpec: XCTestCase { func testEncoding() { let featureFlag = FeatureFlag(flagKey: "unused", variation: 3, version: 2, flagVersion: 5) let flagCounter = FlagCounter() - flagCounter.trackRequest(reportedValue: "a", featureFlag: featureFlag, defaultValue: "b") - flagCounter.trackRequest(reportedValue: "a", featureFlag: featureFlag, defaultValue: "b") + flagCounter.trackRequest(reportedValue: "a", featureFlag: featureFlag, defaultValue: "b", context: LDContext.stub()) + flagCounter.trackRequest(reportedValue: "a", featureFlag: featureFlag, defaultValue: "b", context: LDContext.stub()) encodesToObject(flagCounter) { dict in - XCTAssertEqual(dict.count, 2) + XCTAssertEqual(dict.count, 3) XCTAssertEqual(dict["default"], "b") + XCTAssertEqual(dict["contextKinds"], ["user"]) valueIsArray(dict["counters"]) { counters in XCTAssertEqual(counters.count, 1) valueIsObject(counters[0]) { counter in @@ -158,10 +159,11 @@ final class FlagCounterSpec: XCTestCase { } let flagCounterNulls = FlagCounter() - flagCounterNulls.trackRequest(reportedValue: nil, featureFlag: nil, defaultValue: nil) + flagCounterNulls.trackRequest(reportedValue: nil, featureFlag: nil, defaultValue: nil, context: LDContext.stub()) encodesToObject(flagCounterNulls) { dict in - XCTAssertEqual(dict.count, 2) + XCTAssertEqual(dict.count, 3) XCTAssertEqual(dict["default"], .null) + XCTAssertEqual(dict["contextKinds"], ["user"]) valueIsArray(dict["counters"]) { counters in XCTAssertEqual(counters.count, 1) valueIsObject(counters[0]) { counter in @@ -186,11 +188,11 @@ extension FlagCounter { if flagKey.isKnown { featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: flagKey) for _ in 0.. Void)] = @@ -48,18 +45,16 @@ final class LDConfigSpec: XCTestCase { ("enable background updates", Constants.enableBackgroundUpdates, { c, v in c.enableBackgroundUpdates = v as! Bool }), ("start online", Constants.startOnline, { c, v in c.startOnline = v as! Bool }), ("debug mode", Constants.debugMode, { c, v in c.isDebugMode = v as! Bool }), - ("all user attributes private", Constants.allUserAttributesPrivate, { c, v in c.allUserAttributesPrivate = v as! Bool }), - ("private user attributes", Constants.privateUserAttributes, { c, v in c.privateUserAttributes = (v as! [UserAttribute])}), + ("all context attributes private", Constants.allContextAttributesPrivate, { c, v in c.allContextAttributesPrivate = v as! Bool }), + ("private context attributes", Constants.privateContextAttributes, { c, v in c.privateContextAttributes = (v as! [Reference])}), ("use report", Constants.useReport, { c, v in c.useReport = v as! Bool }), - ("inline user in events", Constants.inlineUserInEvents, { c, v in c.inlineUserInEvents = v as! Bool }), ("evaluation reasons", Constants.evaluationReasons, { c, v in c.evaluationReasons = v as! Bool }), - ("max cached users", Constants.maxCachedUsers, { c, v in c.maxCachedUsers = v as! Int }), + ("max cached contexts", Constants.maxCachedContexts, { c, v in c.maxCachedContexts = v as! Int }), ("diagnostic opt out", Constants.diagnosticOptOut, { c, v in c.diagnosticOptOut = v as! Bool }), ("diagnostic recording interval", Constants.diagnosticRecordingInterval, { c, v in c.diagnosticRecordingInterval = v as! TimeInterval }), ("wrapper name", Constants.wrapperName, { c, v in c.wrapperName = v as! String? }), ("wrapper version", Constants.wrapperVersion, { c, v in c.wrapperVersion = v as! String? }), - ("additional headers", Constants.additionalHeaders, { c, v in c.additionalHeaders = v as! [String: String]}), - ("auto aliasing opt out", Constants.autoAliasingOptOut, { c, v in c.autoAliasingOptOut = v as! Bool })] + ("additional headers", Constants.additionalHeaders, { c, v in c.additionalHeaders = v as! [String: String]})] func testInitDefault() { let config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey) @@ -75,19 +70,17 @@ final class LDConfigSpec: XCTestCase { XCTAssertEqual(config.streamingMode, LDConfig.Defaults.streamingMode) XCTAssertEqual(config.enableBackgroundUpdates, LDConfig.Defaults.enableBackgroundUpdates) XCTAssertEqual(config.startOnline, LDConfig.Defaults.startOnline) - XCTAssertEqual(config.allUserAttributesPrivate, LDConfig.Defaults.allUserAttributesPrivate) - XCTAssertEqual(config.privateUserAttributes, LDConfig.Defaults.privateUserAttributes) + XCTAssertEqual(config.allContextAttributesPrivate, LDConfig.Defaults.allContextAttributesPrivate) + XCTAssertEqual(config.privateContextAttributes, LDConfig.Defaults.privateContextAttributes) XCTAssertEqual(config.useReport, LDConfig.Defaults.useReport) - XCTAssertEqual(config.inlineUserInEvents, LDConfig.Defaults.inlineUserInEvents) XCTAssertEqual(config.isDebugMode, LDConfig.Defaults.debugMode) XCTAssertEqual(config.evaluationReasons, LDConfig.Defaults.evaluationReasons) - XCTAssertEqual(config.maxCachedUsers, LDConfig.Defaults.maxCachedUsers) + XCTAssertEqual(config.maxCachedContexts, LDConfig.Defaults.maxCachedContexts) XCTAssertEqual(config.diagnosticOptOut, LDConfig.Defaults.diagnosticOptOut) XCTAssertEqual(config.diagnosticRecordingInterval, LDConfig.Defaults.diagnosticRecordingInterval) XCTAssertEqual(config.wrapperName, LDConfig.Defaults.wrapperName) XCTAssertEqual(config.wrapperVersion, LDConfig.Defaults.wrapperVersion) XCTAssertEqual(config.additionalHeaders, LDConfig.Defaults.additionalHeaders) - XCTAssertEqual(config.autoAliasingOptOut, LDConfig.Defaults.autoAliasingOptOut) } func testInitUpdate() { @@ -110,19 +103,17 @@ final class LDConfigSpec: XCTestCase { XCTAssertEqual(config.streamingMode, Constants.streamingMode, "\(os)") XCTAssertEqual(config.enableBackgroundUpdates, os.isBackgroundEnabled, "\(os)") XCTAssertEqual(config.startOnline, Constants.startOnline, "\(os)") - XCTAssertEqual(config.allUserAttributesPrivate, Constants.allUserAttributesPrivate, "\(os)") - XCTAssertEqual(config.privateUserAttributes, Constants.privateUserAttributes, "\(os)") + XCTAssertEqual(config.allContextAttributesPrivate, Constants.allContextAttributesPrivate, "\(os)") + XCTAssertEqual(config.privateContextAttributes, Constants.privateContextAttributes, "\(os)") XCTAssertEqual(config.useReport, Constants.useReport, "\(os)") - XCTAssertEqual(config.inlineUserInEvents, Constants.inlineUserInEvents, "\(os)") XCTAssertEqual(config.isDebugMode, Constants.debugMode, "\(os)") XCTAssertEqual(config.evaluationReasons, Constants.evaluationReasons, "\(os)") - XCTAssertEqual(config.maxCachedUsers, Constants.maxCachedUsers, "\(os)") + XCTAssertEqual(config.maxCachedContexts, Constants.maxCachedContexts, "\(os)") XCTAssertEqual(config.diagnosticOptOut, Constants.diagnosticOptOut, "\(os)") XCTAssertEqual(config.diagnosticRecordingInterval, Constants.diagnosticRecordingInterval, "\(os)") XCTAssertEqual(config.wrapperName, Constants.wrapperName, "\(os)") XCTAssertEqual(config.wrapperVersion, Constants.wrapperVersion, "\(os)") XCTAssertEqual(config.additionalHeaders, Constants.additionalHeaders, "\(os)") - XCTAssertEqual(config.autoAliasingOptOut, Constants.autoAliasingOptOut, "\(os)") } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift index 98a641f7..f2f59e54 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift @@ -28,15 +28,12 @@ final class LDUserSpec: QuickSpec { avatar: LDUser.StubConstants.avatar, custom: LDUser.StubConstants.custom(includeSystemValues: true), isAnonymous: LDUser.StubConstants.isAnonymous, - privateAttributes: LDUser.optionalAttributes, - secondary: LDUser.StubConstants.secondary) + privateAttributes: LDUser.optionalAttributes) expect(user.key) == LDUser.StubConstants.key - expect(user.secondary) == LDUser.StubConstants.secondary expect(user.name) == LDUser.StubConstants.name expect(user.firstName) == LDUser.StubConstants.firstName expect(user.lastName) == LDUser.StubConstants.lastName expect(user.isAnonymous) == LDUser.StubConstants.isAnonymous - expect(user.isAnonymousNullable) == LDUser.StubConstants.isAnonymous expect(user.country) == LDUser.StubConstants.country expect(user.ipAddress) == LDUser.StubConstants.ipAddress expect(user.email) == LDUser.StubConstants.email @@ -46,8 +43,6 @@ final class LDUserSpec: QuickSpec { } it("without setting anonymous") { user = LDUser(key: "abc") - expect(user.isAnonymous) == false - expect(user.isAnonymousNullable).to(beNil()) } context("called without optional elements") { var environmentReporter: EnvironmentReporter! @@ -58,7 +53,6 @@ final class LDUserSpec: QuickSpec { it("creates a LDUser without optional elements") { expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) expect(user.isAnonymous) == true - expect(user.isAnonymousNullable) == true expect(user.name).to(beNil()) expect(user.firstName).to(beNil()) @@ -67,11 +61,7 @@ final class LDUserSpec: QuickSpec { expect(user.ipAddress).to(beNil()) expect(user.email).to(beNil()) expect(user.avatar).to(beNil()) - expect(user.custom.count) == 2 - expect(user.custom["device"]) == .string(environmentReporter.deviceModel) - expect(user.custom["os"]) == .string(environmentReporter.systemVersion) expect(user.privateAttributes).to(beEmpty()) - expect(user.secondary).to(beNil()) } } context("called without a key multiple times") { @@ -86,7 +76,6 @@ final class LDUserSpec: QuickSpec { users.forEach { user in expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) expect(user.isAnonymous) == true - expect(user.isAnonymousNullable) == true } } } @@ -104,9 +93,7 @@ final class LDUserSpec: QuickSpec { it("creates a user with system values matching the environment reporter") { expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) expect(user.isAnonymous) == true - expect(user.isAnonymousNullable) == true - expect(user.secondary).to(beNil()) expect(user.name).to(beNil()) expect(user.firstName).to(beNil()) expect(user.lastName).to(beNil()) @@ -114,9 +101,6 @@ final class LDUserSpec: QuickSpec { expect(user.ipAddress).to(beNil()) expect(user.email).to(beNil()) expect(user.avatar).to(beNil()) - expect(user.custom.count) == 2 - expect(user.custom["device"]) == .string(environmentReporter.deviceModel) - expect(user.custom["os"]) == .string(environmentReporter.systemVersion) expect(user.privateAttributes).to(beEmpty()) } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserToContextSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserToContextSpec.swift new file mode 100644 index 00000000..8faef78d --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserToContextSpec.swift @@ -0,0 +1,64 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class LDUserToContextSpec: XCTestCase { + func testSimpleUserIsConvertedToSimpleContext() throws { + let user = LDUser(key: "user-key") + let builder = LDContextBuilder(key: "user-key") + let context = try builder.build().get() + let encoder = JSONEncoder() + let encodedContext = try encoder.encode(context) + let encodedUserContext = try encoder.encode(user.toContext().get()) + + XCTAssertEqual(encodedContext, encodedUserContext) + } + + func testComplexUserConversion() throws { + var user = LDUser(key: "user-key") + user.name = "Example user" + user.firstName = "Example" + user.lastName = "user" + user.country = "United States" + user.ipAddress = "192.168.1.1" + user.email = "example@test.com" + user.avatar = "profile.jpg" + user.custom = ["/nested/attribute": "here is a nested attribute"] + user.isAnonymous = true + user.privateAttributes = [UserAttribute("/nested/attribute")] + + var builder = LDContextBuilder(key: "user-key") + builder.name("Example user") + builder.trySetValue("firstName", "Example".toLDValue()) + builder.trySetValue("lastName", "user".toLDValue()) + builder.trySetValue("country", "United States".toLDValue()) + builder.trySetValue("ipAddress", "192.168.1.1".toLDValue()) + builder.trySetValue("email", "example@test.com".toLDValue()) + builder.trySetValue("avatar", "profile.jpg".toLDValue()) + builder.trySetValue("/nested/attribute", "here is a nested attribute".toLDValue()) + builder.anonymous(true) + builder.addPrivateAttribute(Reference(literal: "/nested/attribute")) + + let context = try builder.build().get() + let userContext = try user.toContext().get() + + XCTAssertEqual(context, userContext) + } + + func testUserAttributeRedactionWorksAsExpected() throws { + var user = LDUser(key: "user-key") + user.custom = [ + "a": "should be removed", + "b": "should be retained", + "/nested/attribute/path": "should be removed" + ] + user.privateAttributes = [UserAttribute("a"), UserAttribute("/nested/attribute/path")] + let context = try user.toContext().get() + let output = try JSONEncoder().encode(context) + let outputJson = String(data: output, encoding: .utf8) + + XCTAssertTrue(outputJson!.contains("should be retained")) + XCTAssertFalse(outputJson!.contains("should be removed")) + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index ee3d0bec..cf826c5a 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift @@ -6,9 +6,9 @@ import LDSwiftEventSource @testable import LaunchDarkly final class DarklyServiceSpec: QuickSpec { - + typealias ServiceResponses = (data: Data?, urlResponse: URLResponse?, error: Error?) - + struct Constants { static let eventCount = 3 static let useGetMethod = false @@ -16,7 +16,7 @@ final class DarklyServiceSpec: QuickSpec { } struct TestContext { - let user = LDUser.stub() + let context = LDContext.stub() var config: LDConfig! var serviceMock: DarklyServiceMock! var serviceFactoryMock: ClientServiceMockFactory = ClientServiceMockFactory() @@ -32,9 +32,9 @@ final class DarklyServiceSpec: QuickSpec { config.useReport = useReport config.diagnosticOptOut = diagnosticOptOut serviceMock = DarklyServiceMock(config: config) - service = DarklyService(config: config, user: user, serviceFactory: serviceFactoryMock) + service = DarklyService(config: config, context: context, serviceFactory: serviceFactoryMock) httpHeaders = HTTPHeaders(config: config, environmentReporter: config.environmentReporter) - } + } func runStubbedGet(statusCode: Int, featureFlags: [LDFlagKey: FeatureFlag]? = nil, flagResponseEtag: String? = nil) { serviceMock.stubFlagRequest(statusCode: statusCode, useReport: config.useReport, flagResponseEtag: flagResponseEtag) @@ -45,7 +45,7 @@ final class DarklyServiceSpec: QuickSpec { } } } - + override func spec() { getFeatureFlagsSpec() flagRequestEtagSpec() @@ -106,12 +106,12 @@ final class DarklyServiceSpec: QuickSpec { expect(reportRequestCount) == 0 } it("creates a GET request") { - // GET request url has the form https:///msdk/evalx/users/ + // GET request url has the form https:///msdk/evalx/contexts/ expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasPrefix("/\(DarklyService.FlagRequestPath.get)")).to(beTrue()) - let expectedUser = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) - expect(urlRequest?.url?.lastPathComponent.jsonValue) == expectedUser + let expectedContext = encodeToLDValue(testContext.context, userInfo: [LDContext.UserInfoKeys.includePrivateAttributes: true]) + expect(urlRequest?.url?.lastPathComponent.jsonValue) == expectedContext } else { fail("request path is missing") } @@ -159,12 +159,12 @@ final class DarklyServiceSpec: QuickSpec { expect(reportRequestCount) == 0 } it("creates a GET request") { - // GET request url has the form https:///msdk/evalx/users/ + // GET request url has the form https:///msdk/evalx/contexts/ expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasPrefix("/\(DarklyService.FlagRequestPath.get)")).to(beTrue()) - let expectedUser = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) - expect(urlRequest?.url?.lastPathComponent.jsonValue) == expectedUser + let expectedContext = encodeToLDValue(testContext.context, userInfo: [LDContext.UserInfoKeys.includePrivateAttributes: true]) + expect(urlRequest?.url?.lastPathComponent.jsonValue) == expectedContext } else { fail("request path is missing") } @@ -273,7 +273,7 @@ final class DarklyServiceSpec: QuickSpec { expect(reportRequestCount) == 1 } it("creates a REPORT request") { - // REPORT request url has the form https:///msdk/evalx/user; httpBody contains the user dictionary + // REPORT request url has the form https:///msdk/evalx/context; httpBody contains the context dictionary expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasSuffix(DarklyService.FlagRequestPath.report)).to(beTrue()) @@ -325,7 +325,7 @@ final class DarklyServiceSpec: QuickSpec { expect(reportRequestCount) == 1 } it("creates a REPORT request") { - // REPORT request url has the form https:///msdk/evalx/user; httpBody contains the user dictionary + // REPORT request url has the form https:///msdk/evalx/context; httpBody contains the context dictionary expect(urlRequest?.url?.host) == testContext.config.baseUrl.host if let path = urlRequest?.url?.path { expect(path.hasSuffix(DarklyService.FlagRequestPath.report)).to(beTrue()) @@ -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 = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) - expect(receivedArguments!.url.lastPathComponent.jsonValue) == expectedUser + let expectedContext = encodeToLDValue(testContext.context, userInfo: [LDContext.UserInfoKeys.includePrivateAttributes: true]) + expect(receivedArguments!.url.lastPathComponent.jsonValue) == expectedContext 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 = encodeToLDValue(testContext.user, userInfo: [LDUser.UserInfoKeys.includePrivateAttributes: true]) - expect(try? JSONDecoder().decode(LDValue.self, from: receivedArguments!.connectBody!)) == expectedUser + let expectedContext = encodeToLDValue(testContext.context, userInfo: [LDContext.UserInfoKeys.includePrivateAttributes: true]) + expect(try? JSONDecoder().decode(LDValue.self, from: receivedArguments!.connectBody!)) == expectedContext } } } @@ -590,7 +590,7 @@ final class DarklyServiceSpec: QuickSpec { } it("makes a valid request") { expect(eventRequest).toNot(beNil()) - expect(eventRequest?.allHTTPHeaderFields?[HTTPHeaders.HeaderKey.eventSchema]) == HTTPHeaders.HeaderValue.eventSchema3 + expect(eventRequest?.allHTTPHeaderFields?[HTTPHeaders.HeaderKey.eventSchema]) == HTTPHeaders.HeaderValue.eventSchema4 expect(eventRequest?.allHTTPHeaderFields?[HTTPHeaders.HeaderKey.eventPayloadIDHeader]?.count) == 36 } it("calls completion with data, response, and no error") { @@ -612,7 +612,7 @@ final class DarklyServiceSpec: QuickSpec { } it("makes a valid request") { expect(eventRequest).toNot(beNil()) - expect(eventRequest?.allHTTPHeaderFields?[HTTPHeaders.HeaderKey.eventSchema]) == HTTPHeaders.HeaderValue.eventSchema3 + expect(eventRequest?.allHTTPHeaderFields?[HTTPHeaders.HeaderKey.eventSchema]) == HTTPHeaders.HeaderValue.eventSchema4 expect(eventRequest?.allHTTPHeaderFields?[HTTPHeaders.HeaderKey.eventPayloadIDHeader]?.count) == 36 } it("calls completion with error and no data or response") { diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift index 11cce882..8a9e536a 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift @@ -36,7 +36,7 @@ final class HTTPHeadersSpec: XCTestCase { "\(EnvironmentReportingMock.Constants.systemName)/\(EnvironmentReportingMock.Constants.sdkVersion)") XCTAssertEqual(headers[HTTPHeaders.HeaderKey.contentType], HTTPHeaders.HeaderValue.applicationJson) XCTAssertEqual(headers[HTTPHeaders.HeaderKey.accept], HTTPHeaders.HeaderValue.applicationJson) - XCTAssertEqual(headers[HTTPHeaders.HeaderKey.eventSchema], HTTPHeaders.HeaderValue.eventSchema3) + XCTAssertEqual(headers[HTTPHeaders.HeaderKey.eventSchema], HTTPHeaders.HeaderValue.eventSchema4) } func testDiagnosticRequestDefaultHeaders() { diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift index 76a52eb2..3ce4afe7 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift @@ -18,7 +18,7 @@ final class CacheConverterSpec: XCTestCase { } func testNoKeysGiven() { - CacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: [], maxCachedUsers: 0) + CacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: [], maxCachedContexts: 0) XCTAssertEqual(serviceFactory.makeKeyedValueCacheCallCount, 0) XCTAssertEqual(serviceFactory.makeFeatureFlagCacheCallCount, 0) } @@ -29,7 +29,7 @@ final class CacheConverterSpec: XCTestCase { serviceFactory.makeFeatureFlagCacheReturnValue.keyedValueCache = v7valueCacheMock serviceFactory.makeKeyedValueCacheReturnValue = v7valueCacheMock v7valueCacheMock.dataReturnValue = CacheConverterSpec.upToDateData - CacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: ["key1", "key2"], maxCachedUsers: 0) + CacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: ["key1", "key2"], maxCachedContexts: 0) XCTAssertEqual(serviceFactory.makeFeatureFlagCacheCallCount, 2) XCTAssertEqual(v7valueCacheMock.dataCallCount, 2) } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift index 72f091cf..93c092a7 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift @@ -15,8 +15,8 @@ final class FeatureFlagCacheSpec: XCTestCase { } func testInit() { - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 2) - XCTAssertEqual(flagCache.maxCachedUsers, 2) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 2) + XCTAssertEqual(flagCache.maxCachedContexts, 2) XCTAssertEqual(serviceFactory.makeKeyedValueCacheCallCount, 1) let bundleHashed = Util.sha256base64(Bundle.main.bundleIdentifier!) let keyHashed = Util.sha256base64("abc") @@ -26,36 +26,36 @@ final class FeatureFlagCacheSpec: XCTestCase { } func testRetrieveNoData() { - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 0) - XCTAssertNil(flagCache.retrieveFeatureFlags(userKey: "user1")) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 0) + XCTAssertNil(flagCache.retrieveFeatureFlags(contextKey: "context1")) XCTAssertEqual(mockValueCache.dataCallCount, 1) - XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-\(Util.sha256base64("user1"))") + XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-context1") } func testRetrieveInvalidData() { mockValueCache.dataReturnValue = Data("invalid".utf8) - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) - XCTAssertNil(flagCache.retrieveFeatureFlags(userKey: "user1")) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 1) + XCTAssertNil(flagCache.retrieveFeatureFlags(contextKey: "context1")) } func testRetrieveEmptyData() throws { mockValueCache.dataReturnValue = try JSONEncoder().encode(StoredItemCollection([:])) - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 2) - XCTAssertEqual(flagCache.retrieveFeatureFlags(userKey: "user1")?.count, 0) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 2) + XCTAssertEqual(flagCache.retrieveFeatureFlags(contextKey: "context1")?.count, 0) } func testRetrieveValidData() throws { mockValueCache.dataReturnValue = try JSONEncoder().encode(testFlagCollection) - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) - let retrieved = flagCache.retrieveFeatureFlags(userKey: "user1") + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 1) + let retrieved = flagCache.retrieveFeatureFlags(contextKey: "context1") XCTAssertEqual(retrieved, testFlagCollection.flags) XCTAssertEqual(mockValueCache.dataCallCount, 1) - XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-\(Util.sha256base64("user1"))") + XCTAssertEqual(mockValueCache.dataReceivedForKey, "flags-context1") } func testStoreCacheDisabled() { - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 0) - flagCache.storeFeatureFlags([:], userKey: "user1", lastUpdated: Date()) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 0) + flagCache.storeFeatureFlags([:], contextKey: "context1", lastUpdated: Date()) XCTAssertEqual(mockValueCache.setCallCount, 0) XCTAssertEqual(mockValueCache.dataCallCount, 0) XCTAssertEqual(mockValueCache.removeObjectCallCount, 0) @@ -63,20 +63,20 @@ final class FeatureFlagCacheSpec: XCTestCase { func testStoreEmptyData() throws { let now = Date() - let hashedUserKey = Util.sha256base64("user1") var count = 0 mockValueCache.setCallback = { - if self.mockValueCache.setReceivedArguments?.forKey == "cached-users" { + if self.mockValueCache.setReceivedArguments?.forKey == "cached-contexts" { let setData = self.mockValueCache.setReceivedArguments!.value - XCTAssertEqual(setData, try JSONEncoder().encode([hashedUserKey: now.millisSince1970])) + XCTAssertEqual(setData, try JSONEncoder().encode(["context1": now.millisSince1970])) count += 1 } else if let received = self.mockValueCache.setReceivedArguments { - XCTAssertEqual(received.forKey, "flags-\(hashedUserKey)") + XCTAssertEqual(received.forKey, "flags-context1") + XCTAssertEqual(received.value, try JSONEncoder().encode(StoredItemCollection([:]))) count += 2 } } - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: -1) - flagCache.storeFeatureFlags([:], userKey: "user1", lastUpdated: now) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: -1) + flagCache.storeFeatureFlags([:], contextKey: "context1", lastUpdated: now) XCTAssertEqual(count, 3) } @@ -86,51 +86,51 @@ final class FeatureFlagCacheSpec: XCTestCase { XCTAssertEqual(received.value, try JSONEncoder().encode(self.testFlagCollection)) } } - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) - flagCache.storeFeatureFlags(testFlagCollection.flags, userKey: "user1", lastUpdated: Date()) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 1) + flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: "context1", lastUpdated: Date()) XCTAssertEqual(mockValueCache.setCallCount, 2) } - func testStoreMaxCachedUsersStored() throws { - let hashedUserKey = Util.sha256base64("user1") + func testStoreMaxCachedContextsStored() throws { + let hashedContextKey = Util.sha256base64("context1") let now = Date() let earlier = now.addingTimeInterval(-30.0) mockValueCache.dataReturnValue = try JSONEncoder().encode(["key1": earlier.millisSince1970]) - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) - flagCache.storeFeatureFlags(testFlagCollection.flags, userKey: "user1", lastUpdated: now) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 1) + flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: hashedContextKey, lastUpdated: now) XCTAssertEqual(mockValueCache.removeObjectCallCount, 1) XCTAssertEqual(mockValueCache.removeObjectReceivedForKey, "flags-key1") let setMetadata = try JSONDecoder().decode([String: Int64].self, from: mockValueCache.setReceivedArguments!.value) - XCTAssertEqual(setMetadata, [hashedUserKey: now.millisSince1970]) + XCTAssertEqual(setMetadata, [hashedContextKey: now.millisSince1970]) } - func testStoreAboveMaxCachedUsersStored() throws { - let hashedUserKey = Util.sha256base64("user1") + func testStoreAboveMaxCachedContextsStored() throws { + let hashedContextKey = Util.sha256base64("context1") let now = Date() let earlier = now.addingTimeInterval(-30.0) let later = now.addingTimeInterval(30.0) mockValueCache.dataReturnValue = try JSONEncoder().encode(["key1": now.millisSince1970, "key2": earlier.millisSince1970, "key3": later.millisSince1970]) - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 2) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 2) var removedObjects: [String] = [] mockValueCache.removeObjectCallback = { removedObjects.append(self.mockValueCache.removeObjectReceivedForKey!) } - flagCache.storeFeatureFlags(testFlagCollection.flags, userKey: "user1", lastUpdated: later) + flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: hashedContextKey, lastUpdated: later) XCTAssertEqual(mockValueCache.removeObjectCallCount, 2) XCTAssertTrue(removedObjects.contains("flags-key1")) XCTAssertTrue(removedObjects.contains("flags-key2")) let setMetadata = try JSONDecoder().decode([String: Int64].self, from: mockValueCache.setReceivedArguments!.value) - XCTAssertEqual(setMetadata, [hashedUserKey: later.millisSince1970, "key3": later.millisSince1970]) + XCTAssertEqual(setMetadata, [hashedContextKey: later.millisSince1970, "key3": later.millisSince1970]) } func testStoreInvalidMetadataStored() throws { - let hashedUserKey = Util.sha256base64("user1") + let hashedContxtKey = Util.sha256base64("context1") let now = Date() mockValueCache.dataReturnValue = try JSONEncoder().encode(["key1": "123"]) - let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedUsers: 1) - flagCache.storeFeatureFlags(testFlagCollection.flags, userKey: "user1", lastUpdated: now) + let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 1) + flagCache.storeFeatureFlags(testFlagCollection.flags, contextKey: hashedContxtKey, lastUpdated: now) XCTAssertEqual(mockValueCache.removeObjectCallCount, 0) let setMetadata = try JSONDecoder().decode([String: Int64].self, from: mockValueCache.setReceivedArguments!.value) - XCTAssertEqual(setMetadata, [hashedUserKey: now.millisSince1970]) + XCTAssertEqual(setMetadata, [hashedContxtKey: now.millisSince1970]) } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift index 1e0d177e..6bf4e29b 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift @@ -12,17 +12,17 @@ final class EventReporterSpec: QuickSpec { struct TestContext { var eventReporter: EventReporter! var config: LDConfig! - var user: LDUser! + var context: LDContext! var serviceMock: DarklyServiceMock! var events: [Event] = [] - var lastEventResponseDate: Date? + var lastEventResponseDate: Date var eventStubResponseDate: Date? var syncResult: SynchronizingError? = nil var diagnosticCache: DiagnosticCachingMock init(eventCount: Int = 0, eventFlushInterval: TimeInterval? = nil, - lastEventResponseDate: Date? = nil, + lastEventResponseDate: Date = Date.distantPast, stubResponseSuccess: Bool = true, stubResponseOnly: Bool = false, stubResponseErrorOnly: Bool = false, @@ -33,7 +33,7 @@ final class EventReporterSpec: QuickSpec { config.eventCapacity = Event.Kind.allKinds.count config.eventFlushInterval = eventFlushInterval ?? Constants.eventFlushInterval - user = LDUser.stub() + context = LDContext.stub() self.eventStubResponseDate = eventStubResponseDate?.adjustedForHttpUrlHeaderUse serviceMock = DarklyServiceMock() @@ -43,10 +43,10 @@ final class EventReporterSpec: QuickSpec { diagnosticCache = DiagnosticCachingMock() serviceMock.diagnosticCache = diagnosticCache - self.lastEventResponseDate = lastEventResponseDate?.adjustedForHttpUrlHeaderUse + self.lastEventResponseDate = lastEventResponseDate.adjustedForHttpUrlHeaderUse eventReporter = EventReporter(service: serviceMock, onSyncComplete: onSyncComplete) (0.. 7.1' + pod 'LaunchDarkly', '~> 8.0' end ``` @@ -71,7 +71,7 @@ To use the [Carthage](https://github.com/Carthage/Carthage) dependency manager t To integrate LaunchDarkly into your Xcode project using Carthage, specify it in your `Cartfile`: ```ogdl -github "launchdarkly/ios-client-sdk" ~> 7.1 +github "launchdarkly/ios-client-sdk" ~> 8.0 ``` ### Manual installation