1 // Copyright 2022-present 650 Industries. All rights reserved.
2 
3 // MARK: - Arguments
4 
5 /**
6  Tries to cast a given value to the type that is wrapped by the dynamic type.
7  - Parameters:
8   - value: A value to be cast. If it's a ``JavaScriptValue``, it's first unpacked to the raw value.
9   - type: Something that implements ``AnyDynamicType`` and knows how to cast the argument.
10  - Returns: A new value converted according to the dynamic type.
11  - Throws: Rethrows various exceptions that could be thrown by the dynamic types.
12  */
castnull13 internal func cast(_ value: Any, toType type: AnyDynamicType, appContext: AppContext) throws -> Any {
14   if let dynamicJSType = type as? DynamicJavaScriptType, dynamicJSType.equals(~JavaScriptValue.self)  {
15     return value
16   }
17   if !(type is DynamicTypedArrayType), let value = value as? JavaScriptValue {
18     return try type.cast(value.getRaw(), appContext: appContext)
19   }
20   return try type.cast(value, appContext: appContext)
21 }
22 
23 /**
24  Tries to cast the given arguments to the types expected by the function.
25  - Parameters:
26    - arguments: An array of arguments to be cast.
27    - function: A function for which to cast the arguments.
28    - appContext: A context of the app.
29  - Returns: An array of arguments after casting. Its size is the same as the input arrays.
30  - Throws: `InvalidArgsNumberException` when the number of arguments is not equal to the actual number
31  of function's arguments (without an owner and promise). Rethrows exceptions thrown by `cast(_:toType:)`.
32  */
castnull33 internal func cast(arguments: [Any], forFunction function: AnyFunction, appContext: AppContext) throws -> [Any] {
34   return try arguments.enumerated().map { index, argument in
35     let argumentType = function.dynamicArgumentTypes[index]
36 
37     do {
38       return try cast(argument, toType: argumentType, appContext: appContext)
39     } catch {
40       throw ArgumentCastException((index: index, type: argumentType)).causedBy(error)
41     }
42   }
43 }
44 
45 /**
46  Casts an array of JavaScript values to non-JavaScript types.
47  */
castnull48 internal func cast(jsValues: [Any], forFunction function: AnyFunction, appContext: AppContext) throws -> [Any] {
49   // TODO: Replace `[Any]` with `[JavaScriptValue]` once we make sure only JS values are passed here
50   return try jsValues.enumerated().map { index, jsValue in
51     let type = function.dynamicArgumentTypes[index]
52 
53     do {
54       // Temporarily some values might already be cast to primitive types, so make sure we cast only `JavaScriptValue` and leave the others as they are.
55       if let jsValue = jsValue as? JavaScriptValue {
56         return try type.cast(jsValue: jsValue, appContext: appContext)
57       } else {
58         return jsValue
59       }
60     } catch {
61       throw ArgumentCastException((index: index, type: type)).causedBy(error)
62     }
63   }
64 }
65 
66 /**
67  Validates whether the number of received arguments is enough to call the given function.
68  Throws `InvalidArgsNumberException` otherwise.
69  */
validateArgumentsNumbernull70 internal func validateArgumentsNumber(function: AnyFunction, received: Int) throws {
71   let argumentsCount = function.argumentsCount
72   let requiredArgumentsCount = function.requiredArgumentsCount
73 
74   if received < requiredArgumentsCount || received > argumentsCount {
75     throw InvalidArgsNumberException((
76       received: received,
77       expected: argumentsCount,
78       required: requiredArgumentsCount
79     ))
80   }
81 }
82 
83 /**
84  Ensures the provided array of arguments matches the number of arguments expected by the function.
85  - If the function takes the owner, it's added to the beginning.
86  - If the array is still too small, missing arguments are very likely to be optional so it puts `nil` in their place.
87  */
88 internal func concat(
89   arguments: [Any],
90   withOwner owner: AnyObject?,
91   withPromise promise: Promise?,
92   forFunction function: AnyFunction,
93   appContext: AppContext
94 ) -> [Any] {
95   var result = arguments
96 
97   if function.takesOwner {
98     result = [owner] + arguments
99   }
100   if arguments.count < function.argumentsCount {
101     result += Array(repeating: Any?.none as Any, count: function.argumentsCount - arguments.count)
102   }
103   // Add promise to the array of arguments if necessary.
104   if let promise {
105     result += [promise]
106   }
107   return result
108 }
109 
110 // MARK: - Exceptions
111 
112 internal class InvalidArgsNumberException: GenericException<(received: Int, expected: Int, required: Int)> {
113   override var reason: String {
114     if param.required < param.expected {
115       return "Received \(param.received) arguments, but \(param.expected) was expected and at least \(param.required) is required"
116     } else {
117       return "Received \(param.received) arguments, but \(param.expected) was expected"
118     }
119   }
120 }
121 
122 internal class ArgumentCastException: GenericException<(index: Int, type: AnyDynamicType)> {
123   override var reason: String {
124     "The \(formatOrdinalNumber(param.index + 1)) argument cannot be cast to type \(param.type.description)"
125   }
126 
formatOrdinalNumbernull127   func formatOrdinalNumber(_ number: Int) -> String {
128     let formatter = NumberFormatter()
129     formatter.numberStyle = .ordinal
130     formatter.locale = Locale(identifier: "en_US")
131     return formatter.string(from: NSNumber(value: number)) ?? ""
132   }
133 }
134 
135 private class ModuleUnavailableException: GenericException<String> {
136   override var reason: String {
137     "Module '\(param)' is no longer available"
138   }
139 }
140