Skip to content

Commit

Permalink
iOS: port async WrappedFunction update (#244)
Browse files Browse the repository at this point in the history
* port async WrappedFunction update

* expose decode for JSValue

* port BaseGenericWrappedAsset changes
  • Loading branch information
hborawski authored Nov 17, 2023
1 parent c2d6b2a commit bc9586c
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 24 deletions.
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

0 comments on commit bc9586c

Please sign in to comment.