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