16ad80deeSTomasz Sapeta // Copyright 2022-present 650 Industries. All rights reserved.
26ad80deeSTomasz Sapeta
3d2f2d83cSTomasz Sapeta // MARK: - Arguments
4d2f2d83cSTomasz Sapeta
5c4fc6f47STomasz Sapeta /**
69b8bcdc4STomasz Sapeta Tries to cast a given value to the type that is wrapped by the dynamic type.
7d2f2d83cSTomasz Sapeta - Parameters:
89b8bcdc4STomasz Sapeta - value: A value to be cast. If it's a ``JavaScriptValue``, it's first unpacked to the raw value.
99b8bcdc4STomasz Sapeta - type: Something that implements ``AnyDynamicType`` and knows how to cast the argument.
104cafe471STomasz Sapeta - Returns: A new value converted according to the dynamic type.
114cafe471STomasz Sapeta - Throws: Rethrows various exceptions that could be thrown by the dynamic types.
12c4fc6f47STomasz Sapeta */
castnull13553b6180STomasz Sapeta internal func cast(_ value: Any, toType type: AnyDynamicType, appContext: AppContext) throws -> Any {
14*45a388e1SŁukasz Kosmaty if let dynamicJSType = type as? DynamicJavaScriptType, dynamicJSType.equals(~JavaScriptValue.self) {
15*45a388e1SŁukasz Kosmaty return value
16*45a388e1SŁukasz Kosmaty }
17002d516eSTomasz Sapeta if !(type is DynamicTypedArrayType), let value = value as? JavaScriptValue {
18553b6180STomasz Sapeta return try type.cast(value.getRaw(), appContext: appContext)
1978516026STomasz Sapeta }
20553b6180STomasz Sapeta return try type.cast(value, appContext: appContext)
21c4fc6f47STomasz Sapeta }
22c4fc6f47STomasz Sapeta
23d2f2d83cSTomasz Sapeta /**
249b8bcdc4STomasz Sapeta Tries to cast the given arguments to the types expected by the function.
25d2f2d83cSTomasz Sapeta - Parameters:
26d2f2d83cSTomasz Sapeta - arguments: An array of arguments to be cast.
279b8bcdc4STomasz Sapeta - function: A function for which to cast the arguments.
28553b6180STomasz Sapeta - appContext: A context of the app.
29d2f2d83cSTomasz Sapeta - Returns: An array of arguments after casting. Its size is the same as the input arrays.
309b8bcdc4STomasz Sapeta - Throws: `InvalidArgsNumberException` when the number of arguments is not equal to the actual number
319b8bcdc4STomasz Sapeta of function's arguments (without an owner and promise). Rethrows exceptions thrown by `cast(_:toType:)`.
32d2f2d83cSTomasz Sapeta */
castnull33553b6180STomasz Sapeta internal func cast(arguments: [Any], forFunction function: AnyFunction, appContext: AppContext) throws -> [Any] {
34d2f2d83cSTomasz Sapeta return try arguments.enumerated().map { index, argument in
353eaf6df6STomasz Sapeta let argumentType = function.dynamicArgumentTypes[index]
36d2f2d83cSTomasz Sapeta
37d2f2d83cSTomasz Sapeta do {
38553b6180STomasz Sapeta return try cast(argument, toType: argumentType, appContext: appContext)
39d2f2d83cSTomasz Sapeta } catch {
40d2f2d83cSTomasz Sapeta throw ArgumentCastException((index: index, type: argumentType)).causedBy(error)
41d2f2d83cSTomasz Sapeta }
42d2f2d83cSTomasz Sapeta }
43d2f2d83cSTomasz Sapeta }
44d2f2d83cSTomasz Sapeta
459b8bcdc4STomasz Sapeta /**
463eaf6df6STomasz Sapeta Casts an array of JavaScript values to non-JavaScript types.
473eaf6df6STomasz Sapeta */
castnull483eaf6df6STomasz Sapeta internal func cast(jsValues: [Any], forFunction function: AnyFunction, appContext: AppContext) throws -> [Any] {
493eaf6df6STomasz Sapeta // TODO: Replace `[Any]` with `[JavaScriptValue]` once we make sure only JS values are passed here
503eaf6df6STomasz Sapeta return try jsValues.enumerated().map { index, jsValue in
513eaf6df6STomasz Sapeta let type = function.dynamicArgumentTypes[index]
523eaf6df6STomasz Sapeta
533eaf6df6STomasz Sapeta do {
543eaf6df6STomasz Sapeta // Temporarily some values might already be cast to primitive types, so make sure we cast only `JavaScriptValue` and leave the others as they are.
553eaf6df6STomasz Sapeta if let jsValue = jsValue as? JavaScriptValue {
563eaf6df6STomasz Sapeta return try type.cast(jsValue: jsValue, appContext: appContext)
573eaf6df6STomasz Sapeta } else {
583eaf6df6STomasz Sapeta return jsValue
593eaf6df6STomasz Sapeta }
603eaf6df6STomasz Sapeta } catch {
613eaf6df6STomasz Sapeta throw ArgumentCastException((index: index, type: type)).causedBy(error)
623eaf6df6STomasz Sapeta }
633eaf6df6STomasz Sapeta }
643eaf6df6STomasz Sapeta }
653eaf6df6STomasz Sapeta
663eaf6df6STomasz Sapeta /**
673eaf6df6STomasz Sapeta Validates whether the number of received arguments is enough to call the given function.
683eaf6df6STomasz Sapeta Throws `InvalidArgsNumberException` otherwise.
693eaf6df6STomasz Sapeta */
validateArgumentsNumbernull703eaf6df6STomasz Sapeta internal func validateArgumentsNumber(function: AnyFunction, received: Int) throws {
713eaf6df6STomasz Sapeta let argumentsCount = function.argumentsCount
723eaf6df6STomasz Sapeta let requiredArgumentsCount = function.requiredArgumentsCount
733eaf6df6STomasz Sapeta
743eaf6df6STomasz Sapeta if received < requiredArgumentsCount || received > argumentsCount {
753eaf6df6STomasz Sapeta throw InvalidArgsNumberException((
763eaf6df6STomasz Sapeta received: received,
773eaf6df6STomasz Sapeta expected: argumentsCount,
783eaf6df6STomasz Sapeta required: requiredArgumentsCount
793eaf6df6STomasz Sapeta ))
803eaf6df6STomasz Sapeta }
813eaf6df6STomasz Sapeta }
823eaf6df6STomasz Sapeta
833eaf6df6STomasz Sapeta /**
844a8d2497STomasz Sapeta Ensures the provided array of arguments matches the number of arguments expected by the function.
854a8d2497STomasz Sapeta - If the function takes the owner, it's added to the beginning.
864a8d2497STomasz Sapeta - If the array is still too small, missing arguments are very likely to be optional so it puts `nil` in their place.
879b8bcdc4STomasz Sapeta */
883eaf6df6STomasz Sapeta internal func concat(
893eaf6df6STomasz Sapeta arguments: [Any],
903eaf6df6STomasz Sapeta withOwner owner: AnyObject?,
913eaf6df6STomasz Sapeta withPromise promise: Promise?,
923eaf6df6STomasz Sapeta forFunction function: AnyFunction,
933eaf6df6STomasz Sapeta appContext: AppContext
943eaf6df6STomasz Sapeta ) -> [Any] {
954a8d2497STomasz Sapeta var result = arguments
964a8d2497STomasz Sapeta
973eaf6df6STomasz Sapeta if function.takesOwner {
984a8d2497STomasz Sapeta result = [owner] + arguments
999b8bcdc4STomasz Sapeta }
1004a8d2497STomasz Sapeta if arguments.count < function.argumentsCount {
1014a8d2497STomasz Sapeta result += Array(repeating: Any?.none as Any, count: function.argumentsCount - arguments.count)
1024a8d2497STomasz Sapeta }
1033eaf6df6STomasz Sapeta // Add promise to the array of arguments if necessary.
1043eaf6df6STomasz Sapeta if let promise {
1053eaf6df6STomasz Sapeta result += [promise]
1063eaf6df6STomasz Sapeta }
1074a8d2497STomasz Sapeta return result
1089b8bcdc4STomasz Sapeta }
1099b8bcdc4STomasz Sapeta
110447e3428STomasz Sapeta // MARK: - Exceptions
111447e3428STomasz Sapeta
1124a8d2497STomasz Sapeta internal class InvalidArgsNumberException: GenericException<(received: Int, expected: Int, required: Int)> {
113d2f2d83cSTomasz Sapeta override var reason: String {
1144a8d2497STomasz Sapeta if param.required < param.expected {
1154a8d2497STomasz Sapeta return "Received \(param.received) arguments, but \(param.expected) was expected and at least \(param.required) is required"
1164a8d2497STomasz Sapeta } else {
1174a8d2497STomasz Sapeta return "Received \(param.received) arguments, but \(param.expected) was expected"
1184a8d2497STomasz Sapeta }
119d2f2d83cSTomasz Sapeta }
120d2f2d83cSTomasz Sapeta }
121d2f2d83cSTomasz Sapeta
1224cafe471STomasz Sapeta internal class ArgumentCastException: GenericException<(index: Int, type: AnyDynamicType)> {
123d2f2d83cSTomasz Sapeta override var reason: String {
12432943018STomasz Sapeta "The \(formatOrdinalNumber(param.index + 1)) argument cannot be cast to type \(param.type.description)"
12532943018STomasz Sapeta }
12632943018STomasz Sapeta
formatOrdinalNumbernull12732943018STomasz Sapeta func formatOrdinalNumber(_ number: Int) -> String {
12832943018STomasz Sapeta let formatter = NumberFormatter()
12932943018STomasz Sapeta formatter.numberStyle = .ordinal
13032943018STomasz Sapeta formatter.locale = Locale(identifier: "en_US")
13132943018STomasz Sapeta return formatter.string(from: NSNumber(value: number)) ?? ""
132d2f2d83cSTomasz Sapeta }
133d2f2d83cSTomasz Sapeta }
134d2f2d83cSTomasz Sapeta
1356ad80deeSTomasz Sapeta private class ModuleUnavailableException: GenericException<String> {
1366ad80deeSTomasz Sapeta override var reason: String {
137d33619e0STomasz Sapeta "Module '\(param)' is no longer available"
1386ad80deeSTomasz Sapeta }
1396ad80deeSTomasz Sapeta }
140