diff --git a/ios/packages/core/Sources/Types/Assets/BaseAssetRegistry.swift b/ios/packages/core/Sources/Types/Assets/BaseAssetRegistry.swift index d02f7c462..b43bcfc38 100644 --- a/ios/packages/core/Sources/Types/Assets/BaseAssetRegistry.swift +++ b/ios/packages/core/Sources/Types/Assets/BaseAssetRegistry.swift @@ -272,7 +272,7 @@ extension JSONDecoder { /// Attempts to decode an object of type T from the JSON data found in value. /// During decoding calls to `decoder.getJSValue()` will return the object subscripted in value /// at the current coding path. A decode function might use this to update RawValueBacked entities. - func decode(_ type: T.Type, from value: JSValue) throws -> T where T: Decodable { + public func decode(_ type: T.Type, from value: JSValue) throws -> T where T: Decodable { setRootJS(value) defer { setRootJS(nil) } diff --git a/ios/packages/core/Sources/Types/Assets/Wrappers.swift b/ios/packages/core/Sources/Types/Assets/Wrappers.swift index b747004aa..a965a9792 100644 --- a/ios/packages/core/Sources/Types/Assets/Wrappers.swift +++ b/ios/packages/core/Sources/Types/Assets/Wrappers.swift @@ -81,11 +81,6 @@ extension AssetData where Self: Equatable { } } -public struct DefaultAdditionalData: Decodable, Equatable { - /// MetaData associated with the asset in this wrapper - public var metaData: MetaData? -} - /** MetaData associated with an asset */ diff --git a/ios/packages/swiftui/Sources/types/WrappedFunction.swift b/ios/packages/swiftui/Sources/types/WrappedFunction.swift index 7ee908cba..e6862dbb9 100644 --- a/ios/packages/swiftui/Sources/types/WrappedFunction.swift +++ b/ios/packages/swiftui/Sources/types/WrappedFunction.swift @@ -51,13 +51,51 @@ public struct WrappedFunction: JSValueBacked, Decodable, Hashable { /** Executes the function and returns the customType specified */ - public func callAsFunction(customType: T.Type, args: Any...) throws -> T? where T: Decodable { + public func callAsFunction(customType: U.Type, args: Any...) throws -> U? where U: Decodable { guard let jsValue = rawValue else { throw DecodingError.malformedData } let decodedState = try JSONDecoder().decode(customType, from: jsValue.call(withArguments: args)) return decodedState } } +extension WrappedFunction where T: Decodable { + public enum Error: Swift.Error, Equatable { + /// There was a failure in the promise from calling a JS function + case promiseFailed(error: String) + } + + public func callAsFunctionAsync(args: Any...) async throws -> T { + try await withCheckedThrowingContinuation { continuation in + guard let jsValue = rawValue else { return continuation.resume(throwing: DecodingError.malformedData) } + + let promiseHandler: @convention(block) (JSValue) -> Void = { + do { + let value = try JSONDecoder().decode(T.self, from: $0) + continuation.resume(returning: value) + } catch { + continuation.resume(throwing: error) + } + } + + let errorHandler: @convention(block) (JSValue) -> Void = { error in + return continuation.resume(throwing: WrappedFunction.Error.promiseFailed(error: error.toString())) + } + + guard + let context = jsValue.context, + let callback = JSValue(object: promiseHandler, in: context), + let catchCallback = JSValue(object: errorHandler, in: context) + else { + return continuation.resume(throwing: PlayerError.jsConversionFailure) + } + + jsValue.call(withArguments: args) + .invokeMethod("then", withArguments: [callback]) + .invokeMethod("catch", withArguments: [catchCallback]) + } + } +} + /** Wrapper class to decode model references into strings from the raw JSValue */ diff --git a/ios/packages/swiftui/Sources/types/assets/WrappedAsset.swift b/ios/packages/swiftui/Sources/types/assets/WrappedAsset.swift index 673bde03f..e1593162e 100644 --- a/ios/packages/swiftui/Sources/types/assets/WrappedAsset.swift +++ b/ios/packages/swiftui/Sources/types/assets/WrappedAsset.swift @@ -15,22 +15,31 @@ ``` { id, type } ``` - This wrapper decodes either to provide a consistent access mechanism */ -public typealias WrappedAsset = GenericWrappedAsset +public typealias WrappedAsset = GenericWrappedAsset + +public typealias GenericWrappedAsset = BaseGenericWrappedAsset -public struct GenericWrappedAsset: Decodable, AssetContainer where AdditionalData: Decodable&Equatable { +public struct DefaultAdditionalData: Decodable, Equatable {} + +public struct BaseGenericWrappedAsset: Decodable, AssetContainer + where MetaData: Decodable&Equatable, AdditionalData: Decodable&Equatable { /// The keys used to decode the wrapper public enum CodingKeys: String, CodingKey { /// Key to decode asset in a wrapper case asset + /// Key to decode metadata in a wrapper + case metaData } /// The underlying asset if it decoded public var asset: SwiftUIAsset? - /// Additional data that is associated with the adjacent asset + /// MetaData that is associated with the adjacent asset + public var metaData: MetaData? + + /// Additional data to decode as sibling keys to `asset` or `metaData` public var additionalData: AdditionalData? /** @@ -53,20 +62,15 @@ public struct GenericWrappedAsset: Decodable, AssetContainer whe public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.asset = try container.decodeIfPresent(RegistryDecodeShim.self, forKey: .asset)?.asset + self.metaData = try container.decodeIfPresent(MetaData.self, forKey: .metaData) self.additionalData = try decoder.singleValueContainer().decode(AdditionalData.self) } } -extension GenericWrappedAsset where AdditionalData == DefaultAdditionalData { - /// MetaData associated with the sibling key `asset` - public var metaData: MetaData? { additionalData?.metaData } -} - // MARK: - Equatable conformance - -extension GenericWrappedAsset: Equatable where AdditionalData: Equatable { +extension GenericWrappedAsset: Equatable where MetaData: Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { - let isMetaDataSame = lhs.additionalData == rhs.additionalData + let isMetaDataSame = lhs.metaData == rhs.metaData let areBothAssetsNil = lhs.asset == nil && rhs.asset == nil var areAssetsEqual: Bool { (lhs.asset?.valueData).flatMap { rhs.asset?.valueData.isEqual($0) } ?? false } return isMetaDataSame && (areBothAssetsNil || areAssetsEqual) diff --git a/ios/packages/swiftui/Tests/SwiftUIRegistryTests.swift b/ios/packages/swiftui/Tests/SwiftUIRegistryTests.swift index 8eddfc141..72cf002e9 100644 --- a/ios/packages/swiftui/Tests/SwiftUIRegistryTests.swift +++ b/ios/packages/swiftui/Tests/SwiftUIRegistryTests.swift @@ -75,7 +75,7 @@ class SwiftUIRegistryTests: XCTestCase { var id: String var type: String - var nested: GenericWrappedAsset? + var nested: BaseGenericWrappedAsset? } class TestNestedAsset: UncontrolledAsset { diff --git a/ios/packages/swiftui/Tests/types/WrappedFunctionTests.swift b/ios/packages/swiftui/Tests/types/WrappedFunctionTests.swift index cdef07dcd..bab154ae2 100644 --- a/ios/packages/swiftui/Tests/types/WrappedFunctionTests.swift +++ b/ios/packages/swiftui/Tests/types/WrappedFunctionTests.swift @@ -14,6 +14,24 @@ import JavaScriptCore class WrappedFunctionTests: XCTestCase { let context: JSContext = JSContext() + private enum PromiseValues: Decodable, Equatable { + case listOfString([String]) + case listOfCustomStruct([CustomStruct]) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + do { + self = .listOfString(try container.decode([String].self)) + } catch { + self = .listOfCustomStruct(try container.decode([CustomStruct].self)) + } + } + } + + private struct CustomStruct: Decodable, Equatable, Encodable { + var someString: String + } + func testWrappedFunction() { let called = expectation(description: "Function Called") let callback: @convention(block) () -> Void = { called.fulfill() } @@ -26,10 +44,6 @@ class WrappedFunctionTests: XCTestCase { wait(for: [called], timeout: 1) } - struct CustomStruct: Decodable, Hashable, Encodable { - var someString: String - } - func testWrappedFunctionWithCustomType() { let called = expectation(description: "Function Called") let callback: @convention(block) (JSValue) -> JSValue = { _ in @@ -50,6 +64,103 @@ class WrappedFunctionTests: XCTestCase { wait(for: [called], timeout: 1) } + func testWrappedFunctionAsyncReturnsInt() async { + JSUtilities.polyfill(self.context) + + let function = self.context + .evaluateScript(""" + (() => { + return new Promise((resolve) => { + setTimeout( + () => { resolve(1) }, + 1000 + ) + }) + }) + """) + + let wrapper = WrappedFunction(rawValue: function) + + do { + let result = try await wrapper.callAsFunctionAsync(args: "") + XCTAssertEqual(result, 1) + } catch { + XCTFail("could not call async wrapped function") + } + } + + func testWrappedFunctionAsyncReturnsStrings() async { + JSUtilities.polyfill(self.context) + + let function = self.context + .evaluateScript(""" + (() => { + return new Promise((resolve) => { + setTimeout( + () => { resolve(["firstString", "secondString"]) }, + 1000 + ) + }) + }) + """) + + let wrapper = WrappedFunction(rawValue: function) + + do { + let result = try await wrapper.callAsFunctionAsync(args: "") + XCTAssertEqual(result, .listOfString(["firstString", "secondString"])) + } catch { + XCTFail("could not call async wrapped function") + } + } + + func testWrappedFunctionAsyncReturnsCustomStructs() async { + JSUtilities.polyfill(self.context) + + let function = self.context + .evaluateScript(""" + (() => { + return new Promise((resolve) => { + setTimeout( + () => { resolve([{someString: 'test1'}, {someString: 'test2'}]) }, + 1000 + ) + }) + }) + """) + + let wrapper = WrappedFunction(rawValue: function) + + do { + let result = try await wrapper.callAsFunctionAsync(args: "") + XCTAssertEqual(result, .listOfCustomStruct([CustomStruct(someString: "test1"), CustomStruct(someString: "test2")])) + } catch { + XCTFail("could not call async wrapped function") + } + } + + func testWrappedFunctionAsyncThrowsError() async { + JSUtilities.polyfill(self.context) + + let function = self.context + .evaluateScript(""" + ( () => Promise.reject(new Error("promise rejected")) ) + """) + + let wrapper = WrappedFunction(rawValue: function) + + do { + _ = try await wrapper.callAsFunctionAsync(args: "") + } catch { + XCTAssertEqual( + WrappedFunction.Error.promiseFailed( + error: """ + (extension in PlayerUI):PlayerUI.WrappedFunction.Error.promiseFailed(error: \"Error: promise rejected\") + """), + WrappedFunction.Error.promiseFailed(error: error.playerDescription)) + } + } + func testWrappedFunctionThrowsError() { let called = expectation(description: "Function Called") let callback: @convention(block) (JSValue) -> JSValue = { _ in