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 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 */ 33 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 */ 48 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 */ 70 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 127 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