From 790603f092bd5a693d3fa9efc135ce455bdc6dde Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Wed, 22 Jun 2022 15:49:56 -0400 Subject: [PATCH] Update Objective C bindings to work with contexts (#213) --- .jazzy.yaml | 2 + LaunchDarkly.xcodeproj/project.pbxproj | 20 ++++ .../Models/Context/Reference.swift | 10 +- .../ObjectiveC/ObjcLDClient.swift | 28 ++--- .../ObjectiveC/ObjcLDContext.swift | 110 ++++++++++++++++++ .../ObjectiveC/ObjcLDReference.swift | 38 ++++++ .../Models/Context/LDContextSpec.swift | 15 ++- 7 files changed, 200 insertions(+), 23 deletions(-) create mode 100644 LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift create mode 100644 LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDReference.swift diff --git a/.jazzy.yaml b/.jazzy.yaml index d9d3f5b1..85be7878 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -47,6 +47,8 @@ custom_categories: - ObjcLDClient - ObjcLDConfig - ObjcLDUser + - ObjcLDReference + - ObjcLDContext - ObjcLDChangedFlag - ObjcLDValue - ObjcLDValueType diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 36033147..0b0e88e8 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -213,6 +213,14 @@ 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 */; }; + 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 */; }; B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */; }; B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4265EB024E7390C001CFD2C /* TestUtil.swift */; }; B467791324D8AEEC00897F00 /* LDSwiftEventSourceStatic in Frameworks */ = {isa = PBXBuildFile; productRef = B467791224D8AEEC00897F00 /* LDSwiftEventSourceStatic */; }; @@ -416,6 +424,8 @@ 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 = ""; }; + 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 = ""; }; B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticCacheSpec.swift; sourceTree = ""; }; B4265EB024E7390C001CFD2C /* TestUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtil.swift; sourceTree = ""; }; B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDEvaluationDetail.swift; sourceTree = ""; }; @@ -634,12 +644,14 @@ 835E1D341F63332C00184DB4 /* ObjectiveC */ = { isa = PBXGroup; children = ( + A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */, 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */, 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */, 835E1D3E1F63450A00184DB4 /* ObjcLDUser.swift */, 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */, B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */, 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */, + A36EDFC72853883400D91B05 /* ObjcLDReference.swift */, ); path = ObjectiveC; sourceTree = ""; @@ -1150,6 +1162,7 @@ 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 */, 831188452113ADC500D77CB5 /* LDClient.swift in Sources */, A310881E2837DC0400184942 /* Kind.swift in Sources */, @@ -1184,6 +1197,7 @@ 8354AC732243166900CDE602 /* FeatureFlagCache.swift in Sources */, 8311885B2113AE1D00D77CB5 /* Throttler.swift in Sources */, 8311884E2113ADE500D77CB5 /* Event.swift in Sources */, + A36EDFCB2853883400D91B05 /* ObjcLDReference.swift in Sources */, 832D68A5224A38FC005F052A /* CacheConverter.swift in Sources */, 831188432113ADBE00D77CB5 /* LDCommon.swift in Sources */, B4C9D4312489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, @@ -1202,6 +1216,7 @@ 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 */, @@ -1212,6 +1227,7 @@ 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 */, @@ -1267,6 +1283,7 @@ 83FEF8DD1F266742001CF12C /* FlagSynchronizer.swift in Sources */, 830BF933202D188E006DF9B1 /* HTTPURLRequest.swift in Sources */, B4C9D4332489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, + A36EDFCD2853C50B00D91B05 /* ObjcLDContext.swift in Sources */, 8354EFE51F263DAC00C05156 /* FeatureFlag.swift in Sources */, 8372668C20D4439600BD1088 /* DateFormatter.swift in Sources */, A310881B2837DC0400184942 /* Kind.swift in Sources */, @@ -1301,6 +1318,7 @@ 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 */, 832D68A2224A38FC005F052A /* CacheConverter.swift in Sources */, 835E1D401F63450A00184DB4 /* ObjcLDConfig.swift in Sources */, @@ -1372,6 +1390,7 @@ 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 */, 83D9EC7D2062DEAB004D7FA6 /* LDChangedFlag.swift in Sources */, A310881C2837DC0400184942 /* Kind.swift in Sources */, @@ -1406,6 +1425,7 @@ C443A40323145FB700145710 /* ConnectionInformation.swift in Sources */, B4C9D4392489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, B468E71124B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, + A36EDFC92853883400D91B05 /* ObjcLDReference.swift in Sources */, 83D9EC952062DEAB004D7FA6 /* Date.swift in Sources */, 832D68A3224A38FC005F052A /* CacheConverter.swift in Sources */, 83D9EC972062DEAB004D7FA6 /* Thread.swift in Sources */, diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift index 4d97f93f..8550dd92 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/Reference.swift @@ -1,6 +1,6 @@ import Foundation -enum ReferenceError: Codable, Equatable, Error { +public enum ReferenceError: Codable, Equatable, Error { case empty case doubleSlash case invalidEscapeSequence @@ -25,7 +25,7 @@ enum ReferenceError: Codable, Equatable, Error { } extension ReferenceError: CustomStringConvertible { - var description: String { + public var description: String { switch self { case .empty: return "empty" case .doubleSlash: return "doubleSlash" @@ -129,11 +129,11 @@ public struct Reference: Codable, Equatable, Hashable { return error == nil } - internal func getError() -> ReferenceError? { + public func getError() -> ReferenceError? { return error } - public func depth() -> Int { + internal func depth() -> Int { return components.count } @@ -141,7 +141,7 @@ public struct Reference: Codable, Equatable, Hashable { return rawPath } - public func component(_ index: Int) -> (String, Int?)? { + internal func component(_ index: Int) -> (String, Int?)? { if index >= self.depth() { return nil } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index 8e73f12a..2803748c 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -110,10 +110,9 @@ public final class ObjcLDClient: NSObject { - parameter user: The ObjcLDUser set with the desired user. */ -// TODO(mmk) Come back to this -// @objc public func identify(context: ObjcLDContext) { -// ldClient.identify(context: context.context, completion: nil) -// } + @objc public func identify(context: ObjcLDContext) { + ldClient.identify(context: context.context, 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. @@ -127,10 +126,9 @@ public final class ObjcLDClient: NSObject { - parameter user: The ObjcLDUser set with the desired user. - parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays. (Optional) */ -// TODO(mmk) Come back to this -// @objc public func identify(context: ObjcLDContext, completion: (() -> Void)? = nil) { -// ldClient.identify(context: context.context, completion: completion) -// } + @objc public func identify(context: ObjcLDContext, completion: (() -> Void)? = nil) { + ldClient.identify(context: context.context, completion: completion) + } /** Stops the LDClient. Stopping the client means the LDClient goes offline and stops recording events. LDClient will no longer provide feature flag values, only returning default values. @@ -552,10 +550,9 @@ public final class ObjcLDClient: NSObject { - parameter completion: Closure called when the embedded `setOnline` call completes. (Optional) */ /// - Tag: start -// TODO(mmk) Come back to this -// @objc public static func start(configuration: ObjcLDConfig, context: ObjcLDContext, completion: (() -> Void)? = nil) { -// LDClient.start(config: configuration.config, context: context.context, completion: completion) -// } + @objc public static func start(configuration: ObjcLDConfig, context: ObjcLDContext, completion: (() -> Void)? = nil) { + LDClient.start(config: configuration.config, context: context.context, completion: completion) + } /** See [start](x-source-tag://start) for more information on starting the SDK. @@ -565,10 +562,9 @@ public final class ObjcLDClient: NSObject { - 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) */ -// TODO(mmk) Come back to this -// @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) -// } + @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) + } private init(client: LDClient) { ldClient = client diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift new file mode 100644 index 00000000..bfe337d8 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDContext.swift @@ -0,0 +1,110 @@ +import Foundation + +@objc(LDContext) +public final class ObjcLDContext: NSObject { + var context: LDContext + + init(_ context: LDContext) { + self.context = context + } + + @objc public func fullyQualifiedKey() -> String { context.fullyQualifiedKey() } + @objc public func isMulti() -> Bool { context.isMulti() } + @objc public func contextKeys() -> [String: String] { context.contextKeys() } + @objc public func getValue(reference: ObjcLDReference) -> ObjcLDValue? { + guard let value = context.getValue(reference.reference) + else { return nil } + + return ObjcLDValue(wrappedValue: value) + } +} + +@objc(LDContextBuilder) +public final class ObjcLDContextBuilder: NSObject { + var builder: LDContextBuilder + + @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 + } + + @objc public func kind(kind: String) { builder.kind(kind) } + @objc public func key(key: String) { builder.key(key) } + @objc public func name(name: String) { builder.name(name) } + @objc public func secondary(secondary: String) { builder.secondary(secondary) } + @objc public func transient(transient: Bool) { builder.transient(transient) } + @objc public func addPrivateAttribute(reference: ObjcLDReference) { builder.addPrivateAttribute(reference.reference) } + @objc public func removePrivateAttribute(reference: ObjcLDReference) { builder.removePrivateAttribute(reference.reference) } + + @discardableResult + @objc public func trySetValue(name: String, value: ObjcLDValue) -> Bool { + builder.trySetValue(name, value.wrappedValue) + } + + @objc public func build() -> ContextBuilderResult { + switch builder.build() { + case .success(let context): + return ContextBuilderResult.fromSuccess(context) + case .failure(let error): + return ContextBuilderResult.fromError(error) + } + } +} + +@objc(LDMultiContextBuilder) +public final class ObjcLDMultiContextBuilder: NSObject { + var builder: LDMultiContextBuilder + + @objc public override init() { + builder = LDMultiContextBuilder() + } + + @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 + } + + @objc public func build() -> ContextBuilderResult { + switch builder.build() { + case .success(let context): + return ContextBuilderResult.fromSuccess(context) + case .failure(let error): + return ContextBuilderResult.fromError(error) + } + } +} + +@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 + } + + public static func fromSuccess(_ success: LDContext) -> ContextBuilderResult { + ContextBuilderResult(success, nil) + } + + 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/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/LaunchDarklyTests/Models/Context/LDContextSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift index ed3bb97a..6a9f569c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift @@ -4,8 +4,6 @@ import XCTest @testable import LaunchDarkly final class LDContextSpec: XCTestCase { - // TOOD(mmk) Make sure we cannot make a context with a kind of kind - func testBuildCanCreateSimpleContext() throws { var builder = LDContextBuilder(key: "context-key") builder.name("Name") @@ -15,6 +13,19 @@ final class LDContextSpec: XCTestCase { 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")