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