From 570ff7c89c23f49b1a0718b68c750b5761818ed3 Mon Sep 17 00:00:00 2001 From: Chloe Han Date: Fri, 9 Aug 2024 10:06:20 -0400 Subject: [PATCH 1/4] ios expose constantsController #446 --- ios/core/Sources/Player/HeadlessPlayer.swift | 11 +++ .../Types/Core/ConstantsController.swift | 31 ++++++ ios/core/Tests/HeadlessPlayerTests.swift | 95 +++++++++++++++++++ ios/swiftui/Sources/SwiftUIPlayer.swift | 12 +++ 4 files changed, 149 insertions(+) create mode 100644 ios/core/Sources/Types/Core/ConstantsController.swift diff --git a/ios/core/Sources/Player/HeadlessPlayer.swift b/ios/core/Sources/Player/HeadlessPlayer.swift index 3399f340a..08ed0f9a0 100644 --- a/ios/core/Sources/Player/HeadlessPlayer.swift +++ b/ios/core/Sources/Player/HeadlessPlayer.swift @@ -137,6 +137,8 @@ public protocol HeadlessPlayer { var hooks: HooksType? { get } /// A logger reference for use in plugins to log through the shared player logger var logger: TapableLogger { get } + /// A reference to the Key/Value store for constants and context for player + var constantsController: ConstantsController? { get } /** Sets up the core javascript player in the given context @@ -169,6 +171,15 @@ public extension HeadlessPlayer { else { return nil } return BaseFlowState.createInstance(value: jsState) } + + var constantsController: ConstantsController? { + guard + let constantControllerJSValue = jsPlayerReference?.objectForKeyedSubscript("constantsController") + else { + return nil + } + return ConstantsController(constantsController: constantControllerJSValue) + } /** Sets up the core javascript player in the given context diff --git a/ios/core/Sources/Types/Core/ConstantsController.swift b/ios/core/Sources/Types/Core/ConstantsController.swift new file mode 100644 index 000000000..ab45fd08e --- /dev/null +++ b/ios/core/Sources/Types/Core/ConstantsController.swift @@ -0,0 +1,31 @@ +import JavaScriptCore + +public class ConstantsController { + var constantsController: JSValue? + + public func getConstants(key: Any, namespace: String, fallback: Any? = nil) -> T? { + if let fallbackValue = fallback { + let value = self.constantsController?.invokeMethod("getConstants", withArguments: [key, namespace, fallbackValue]) + return value?.toString() as? T + } else { + let value = self.constantsController?.invokeMethod("getConstants", withArguments: [key, namespace]) + return value?.toString() as? T + } + } + + public func addConstants(data: Any, namespace: String) -> Void { + self.constantsController?.invokeMethod("addConstants", withArguments: [data, namespace]) + } + + public func setTemporaryValues(data: Any, namespace: String) -> Void { + self.constantsController?.invokeMethod("setTemporaryValues", withArguments: [data, namespace]) + } + + public func clearTemporaryValues() -> Void { + self.constantsController?.invokeMethod("clearTemporaryValues", withArguments: []) + } + + public init(constantsController: JSValue) { + self.constantsController = constantsController + } +} diff --git a/ios/core/Tests/HeadlessPlayerTests.swift b/ios/core/Tests/HeadlessPlayerTests.swift index 9299520e6..39e935f39 100644 --- a/ios/core/Tests/HeadlessPlayerTests.swift +++ b/ios/core/Tests/HeadlessPlayerTests.swift @@ -272,6 +272,101 @@ class HeadlessPlayerTests: XCTestCase { player.start(flow: FlowData.COUNTER) { _ in} wait(for: [updateExp], timeout: 1) } + + func testConstantsController() { + let player = HeadlessPlayerImpl(plugins: []) + + guard let constantsController = player.constantsController else { return } + + // Basic get/set tests + let data: Any = [ + "firstname": "john", + "lastname": "doe", + "favorite": [ + "color": "red" + ] + ] + + constantsController.addConstants(data: data, namespace: "constants") + + let firstname: String? = constantsController.getConstants(key: "firstname", namespace: "constants") + XCTAssertEqual(firstname, "john") + + let middleName: String? = constantsController.getConstants(key:"middlename", namespace: "constants") + XCTAssertEqual(middleName, "undefined") + + let middleNameSafe: String? = constantsController.getConstants(key:"middlename", namespace: "constants", fallback: "A") + XCTAssertEqual(middleNameSafe, "A") + + let favoriteColor: String? = constantsController.getConstants(key:"favorite.color", namespace: "constants") + XCTAssertEqual(favoriteColor, "red") + + let nonExistantNamespace: String? = constantsController.getConstants(key:"test", namespace: "foo") + XCTAssertEqual(nonExistantNamespace, "undefined") + + let nonExistantNamespaceWithFallback: String? = constantsController.getConstants(key:"test", namespace: "foo", fallback: "B") + XCTAssertEqual(nonExistantNamespaceWithFallback, "B") + + // Test and make sure keys override properly + let newData: Any = [ + "favorite": [ + "color": "blue", + ], + ]; + + constantsController.addConstants(data: newData, namespace: "constants"); + + let newFavoriteColor: String? = constantsController.getConstants(key: "favorite.color", namespace:"constants") + XCTAssertEqual(newFavoriteColor, "blue") + } + + func testConstantsControllerTempValues() { + let player = HeadlessPlayerImpl(plugins: []) + + guard let constantsController = player.constantsController else { return } + + // Add initial constants + let data: Any = [ + "firstname": "john", + "lastname": "doe", + "favorite": [ + "color": "red" + ] + ] + constantsController.addConstants(data: data, namespace: "constants") + + // Override with temporary values + let tempData: Any = [ + "firstname": "jane", + "favorite": [ + "color": "blue" + ] + ] + constantsController.setTemporaryValues(data:tempData, namespace: "constants") + + // Test temporary override + let firstnameTemp: String? = constantsController.getConstants(key:"firstname", namespace: "constants") + XCTAssertEqual(firstnameTemp, "jane") + + let favoriteColorTemp: String? = constantsController.getConstants(key: "favorite.color", namespace: "constants") + XCTAssertEqual(favoriteColorTemp, "blue") + + // Test fallback to original values when temporary values are not present + let lastnameTemp: String? = constantsController.getConstants(key: "lastname", namespace: "constants") + XCTAssertEqual(lastnameTemp, "doe") + + // Reset temp and values should be the same as the original data + constantsController.clearTemporaryValues(); + + let firstname: String? = constantsController.getConstants(key:"firstname", namespace: "constants") + XCTAssertEqual(firstname, "john") + + let favoriteColor: String? = constantsController.getConstants(key: "favorite.color", namespace: "constants") + XCTAssertEqual(favoriteColor, "red") + + let lastname: String? = constantsController.getConstants(key: "lastname", namespace: "constants") + XCTAssertEqual(lastname, "doe") + } } class FakePlugin: JSBasePlugin, NativePlugin { diff --git a/ios/swiftui/Sources/SwiftUIPlayer.swift b/ios/swiftui/Sources/SwiftUIPlayer.swift index fb7e66d9f..01f23d9b9 100644 --- a/ios/swiftui/Sources/SwiftUIPlayer.swift +++ b/ios/swiftui/Sources/SwiftUIPlayer.swift @@ -199,6 +199,7 @@ public struct SwiftUIPlayer: View, HeadlessPlayer { public var body: some View { bodyContent .environment(\.inProgressState, (state as? InProgressState)) + .environment(\.constantsController, constantsController) // forward results from our Context along to our result binding .onReceive(context.$result.debounce(for: 0.1, scheduler: RunLoop.main)) { self.result = $0 @@ -232,12 +233,23 @@ struct InProgressStateKey: EnvironmentKey { static var defaultValue: InProgressState? } +/// EnvironmentKey for storing `constantsController` +struct ConstantsControllerStateKey: EnvironmentKey { + /// The default value for `@Environment(\.constantsController)` + static var defaultValue: ConstantsController? = nil +} + public extension EnvironmentValues { /// The `InProgressState` of Player if it is in progress, and in scope var inProgressState: InProgressState? { get { self[InProgressStateKey.self] } set { self[InProgressStateKey.self] = newValue } } + + var constantsController: ConstantsController? { + get { self[ConstantsControllerStateKey.self] } + set { self[ConstantsControllerStateKey.self] = newValue } + } } internal extension SwiftUIPlayer { From 056cea72d5d698da7fe10834a9207cb07214e768 Mon Sep 17 00:00:00 2001 From: Chloe Han Date: Mon, 12 Aug 2024 10:14:16 -0400 Subject: [PATCH 2/4] add docstrings to public funcs --- .../Types/Core/ConstantsController.swift | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/ios/core/Sources/Types/Core/ConstantsController.swift b/ios/core/Sources/Types/Core/ConstantsController.swift index ab45fd08e..681d42205 100644 --- a/ios/core/Sources/Types/Core/ConstantsController.swift +++ b/ios/core/Sources/Types/Core/ConstantsController.swift @@ -2,7 +2,13 @@ import JavaScriptCore public class ConstantsController { var constantsController: JSValue? - + + /// Function to retrieve constants from the providers store + /// - Parameters: + /// - key: Key used for the store access + /// - namespace: Namespace values were loaded under + /// - fallback:Optional - if key doesn't exist in namespace what to return (will return unknown if not provided) + /// - Returns: Constant values from store public func getConstants(key: Any, namespace: String, fallback: Any? = nil) -> T? { if let fallbackValue = fallback { let value = self.constantsController?.invokeMethod("getConstants", withArguments: [key, namespace, fallbackValue]) @@ -12,15 +18,24 @@ public class ConstantsController { return value?.toString() as? T } } - + + /// Function to add constants to the providers store + /// - Parameters: + /// - data: Values to add to the constants store + /// - namespace: Namespace values to be added under public func addConstants(data: Any, namespace: String) -> Void { self.constantsController?.invokeMethod("addConstants", withArguments: [data, namespace]) } - + + /// Function to set values to temporarily override certain keys in the perminant store + /// - Parameters: + /// - data: Values to override store with + /// - namespace: Namespace to override public func setTemporaryValues(data: Any, namespace: String) -> Void { self.constantsController?.invokeMethod("setTemporaryValues", withArguments: [data, namespace]) } + /// Clears any temporary values that were previously set public func clearTemporaryValues() -> Void { self.constantsController?.invokeMethod("clearTemporaryValues", withArguments: []) } From 97ff0e214be107aa766aebcfa97671d54a0fc855 Mon Sep 17 00:00:00 2001 From: Chloe Han Date: Mon, 12 Aug 2024 10:21:51 -0400 Subject: [PATCH 3/4] fix return type and add tests --- ios/core/Sources/Types/Core/ConstantsController.swift | 4 ++-- ios/core/Tests/HeadlessPlayerTests.swift | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/ios/core/Sources/Types/Core/ConstantsController.swift b/ios/core/Sources/Types/Core/ConstantsController.swift index 681d42205..cbd74c854 100644 --- a/ios/core/Sources/Types/Core/ConstantsController.swift +++ b/ios/core/Sources/Types/Core/ConstantsController.swift @@ -12,10 +12,10 @@ public class ConstantsController { public func getConstants(key: Any, namespace: String, fallback: Any? = nil) -> T? { if let fallbackValue = fallback { let value = self.constantsController?.invokeMethod("getConstants", withArguments: [key, namespace, fallbackValue]) - return value?.toString() as? T + return value?.toObject() as? T } else { let value = self.constantsController?.invokeMethod("getConstants", withArguments: [key, namespace]) - return value?.toString() as? T + return value?.toObject() as? T } } diff --git a/ios/core/Tests/HeadlessPlayerTests.swift b/ios/core/Tests/HeadlessPlayerTests.swift index 39e935f39..26b4062e9 100644 --- a/ios/core/Tests/HeadlessPlayerTests.swift +++ b/ios/core/Tests/HeadlessPlayerTests.swift @@ -284,7 +284,8 @@ class HeadlessPlayerTests: XCTestCase { "lastname": "doe", "favorite": [ "color": "red" - ] + ], + "age": 1 ] constantsController.addConstants(data: data, namespace: "constants") @@ -293,16 +294,19 @@ class HeadlessPlayerTests: XCTestCase { XCTAssertEqual(firstname, "john") let middleName: String? = constantsController.getConstants(key:"middlename", namespace: "constants") - XCTAssertEqual(middleName, "undefined") + XCTAssertNil(middleName) let middleNameSafe: String? = constantsController.getConstants(key:"middlename", namespace: "constants", fallback: "A") XCTAssertEqual(middleNameSafe, "A") let favoriteColor: String? = constantsController.getConstants(key:"favorite.color", namespace: "constants") XCTAssertEqual(favoriteColor, "red") + + let age: Int? = constantsController.getConstants(key:"age", namespace: "constants") + XCTAssertEqual(age, 1) let nonExistantNamespace: String? = constantsController.getConstants(key:"test", namespace: "foo") - XCTAssertEqual(nonExistantNamespace, "undefined") + XCTAssertNil(nonExistantNamespace) let nonExistantNamespaceWithFallback: String? = constantsController.getConstants(key:"test", namespace: "foo", fallback: "B") XCTAssertEqual(nonExistantNamespaceWithFallback, "B") From 08cbaea70dec8dfcfddd0d85637dc60ce111b7ca Mon Sep 17 00:00:00 2001 From: Chloe Han Date: Wed, 14 Aug 2024 10:08:37 -0400 Subject: [PATCH 4/4] add comments --- ios/core/Sources/Player/HeadlessPlayer.swift | 1 + ios/swiftui/Sources/SwiftUIPlayer.swift | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ios/core/Sources/Player/HeadlessPlayer.swift b/ios/core/Sources/Player/HeadlessPlayer.swift index 08ed0f9a0..22b8be28a 100644 --- a/ios/core/Sources/Player/HeadlessPlayer.swift +++ b/ios/core/Sources/Player/HeadlessPlayer.swift @@ -172,6 +172,7 @@ public extension HeadlessPlayer { return BaseFlowState.createInstance(value: jsState) } + /// The constants and context for player var constantsController: ConstantsController? { guard let constantControllerJSValue = jsPlayerReference?.objectForKeyedSubscript("constantsController") diff --git a/ios/swiftui/Sources/SwiftUIPlayer.swift b/ios/swiftui/Sources/SwiftUIPlayer.swift index 01f23d9b9..2be8973d4 100644 --- a/ios/swiftui/Sources/SwiftUIPlayer.swift +++ b/ios/swiftui/Sources/SwiftUIPlayer.swift @@ -245,7 +245,8 @@ public extension EnvironmentValues { get { self[InProgressStateKey.self] } set { self[InProgressStateKey.self] = newValue } } - + + /// The ConstantsController reference of Player var constantsController: ConstantsController? { get { self[ConstantsControllerStateKey.self] } set { self[ConstantsControllerStateKey.self] = newValue }