Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iOS: port async WrappedFunction update #244

Merged
merged 4 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(_ type: T.Type, from value: JSValue) throws -> T where T: Decodable {
public func decode<T>(_ type: T.Type, from value: JSValue) throws -> T where T: Decodable {
setRootJS(value)
defer { setRootJS(nil) }

Expand Down
5 changes: 0 additions & 5 deletions ios/packages/core/Sources/Types/Assets/Wrappers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
40 changes: 39 additions & 1 deletion ios/packages/swiftui/Sources/types/WrappedFunction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,51 @@ public struct WrappedFunction<T>: JSValueBacked, Decodable, Hashable {
/**
Executes the function and returns the customType specified
*/
public func callAsFunction<T>(customType: T.Type, args: Any...) throws -> T? where T: Decodable {
public func callAsFunction<U>(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
*/
Expand Down
28 changes: 16 additions & 12 deletions ios/packages/swiftui/Sources/types/assets/WrappedAsset.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,31 @@
```
{ id, type }
```

This wrapper decodes either to provide a consistent access mechanism
*/
public typealias WrappedAsset = GenericWrappedAsset<DefaultAdditionalData>
public typealias WrappedAsset = GenericWrappedAsset<MetaData>

public typealias GenericWrappedAsset<MetaDataType: Decodable&Equatable> = BaseGenericWrappedAsset<MetaDataType, DefaultAdditionalData>

public struct GenericWrappedAsset<AdditionalData>: Decodable, AssetContainer where AdditionalData: Decodable&Equatable {
public struct DefaultAdditionalData: Decodable, Equatable {}

public struct BaseGenericWrappedAsset<MetaData, AdditionalData>: 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?

/**
Expand All @@ -53,20 +62,15 @@ public struct GenericWrappedAsset<AdditionalData>: Decodable, AssetContainer whe
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.asset = try container.decodeIfPresent(RegistryDecodeShim<SwiftUIAsset>.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)
Expand Down
2 changes: 1 addition & 1 deletion ios/packages/swiftui/Tests/SwiftUIRegistryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class SwiftUIRegistryTests: XCTestCase {
var id: String
var type: String

var nested: GenericWrappedAsset<TestAddedData>?
var nested: BaseGenericWrappedAsset<MetaData, TestAddedData>?
}

class TestNestedAsset: UncontrolledAsset<TestData> {
Expand Down
119 changes: 115 additions & 4 deletions ios/packages/swiftui/Tests/types/WrappedFunctionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
Expand All @@ -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
Expand All @@ -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<Int>(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<PromiseValues>(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<PromiseValues>(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<Int>(rawValue: function)

do {
_ = try await wrapper.callAsFunctionAsync(args: "")
} catch {
XCTAssertEqual(
WrappedFunction<Int>.Error.promiseFailed(
error: """
(extension in PlayerUI):PlayerUI.WrappedFunction<Swift.Int>.Error.promiseFailed(error: \"Error: promise rejected\")
"""),
WrappedFunction<Int>.Error.promiseFailed(error: error.playerDescription))
}
}

func testWrappedFunctionThrowsError() {
let called = expectation(description: "Function Called")
let callback: @convention(block) (JSValue) -> JSValue = { _ in
Expand Down