1 // Copyright 2023-present 650 Industries. All rights reserved.
2 
3 /**
4  Represents a JavaScript function that can be called by the native code and that must return the given generic `ReturnType`.
5  */
6 public final class JavaScriptFunction<ReturnType>: AnyArgument, AnyJavaScriptValue {
7   /**
8    Raw representation of the JavaScript function that doesn't impose any restrictions on the returned type.
9    */
10   private let rawFunction: RawJavaScriptFunction
11 
12   /**
13    Weak reference to the app context that is necessary to convert some arguments associated with the context (e.g. shared objects).
14    */
15   private weak var appContext: AppContext?
16 
17   init(rawFunction: RawJavaScriptFunction, appContext: AppContext) {
18     self.rawFunction = rawFunction
19     self.appContext = appContext
20   }
21 
22   // MARK: - Calling
23 
24   /**
25    Calls the function with the given `this` object and arguments.
26    */
27   public func call(_ arguments: Any..., usingThis this: JavaScriptObject? = nil) throws -> ReturnType {
28     return try call(withArguments: arguments, asConstructor: false, usingThis: this)
29   }
30 
31   /**
32    Calls the function as a constructor with the given arguments. It's like calling a function with the `new` keyword.
33    */
34   public func callAsConstructor(_ arguments: Any...) throws -> ReturnType {
35     return try call(withArguments: arguments, asConstructor: true, usingThis: nil)
36   }
37 
38   /**
39    Universal function that calls the function with given arguments, this object and whether to call it as a constructor.
40    */
41   private func call(withArguments arguments: [Any] = [], asConstructor: Bool = false, usingThis this: JavaScriptObject? = nil) throws -> ReturnType {
42     guard let appContext else {
43       throw AppContextLostException()
44     }
45     let value = rawFunction.call(withArguments: arguments, thisObject: this, asConstructor: false)
46     let dynamicType = ~ReturnType.self
47 
48     guard let result = try dynamicType.cast(jsValue: value, appContext: appContext) as? ReturnType else {
49       throw UnexpectedReturnType(dynamicType.description)
50     }
51     return result
52   }
53 
54   // MARK: - AnyJavaScriptValue
55 
56   internal static func convert(from value: JavaScriptValue, appContext: AppContext) throws -> Self {
57     guard value.kind == .function else {
58       throw Conversions.ConvertingException<JavaScriptFunction<ReturnType>>(value)
59     }
60     return Self(rawFunction: value.getFunction(), appContext: appContext)
61   }
62 }
63 
64 private final class UnexpectedReturnType: GenericException<String> {
65   override var reason: String {
66     return "The function returned a value that cannot be converted to \(param)"
67   }
68 }
69