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