From 1cf94e0f5954d037237aa95a0987899fec013eaa Mon Sep 17 00:00:00 2001 From: avmushtukov Date: Thu, 5 Dec 2024 10:24:04 +0300 Subject: [PATCH] add support for array methods and fix issues with tests commit_hash:edee6544bfe24236ce733a5d0545f7ec18d2293c --- .../BuiltinFunctionProviderGenerator.kt | 3 +- .../div/evaluable/EvaluableException.kt | 2 +- .../div/evaluable/function/ArrayFunctions.kt | 140 +++++++++++++----- .../function/BuiltinFunctionProvider.kt | 9 ++ .../evaluable/function/FunctionRegistry.kt | 2 +- .../evaluable/function/FunctionValidator.kt | 12 +- .../expression_test_data/methods_array.json | 39 +++-- .../expression_test_data/methods_dict.json | 6 +- .../methods_signatures_array.json | 27 ++-- 9 files changed, 176 insertions(+), 64 deletions(-) diff --git a/client/android/div-evaluable/src/generator/java/com/yandex/div/evaluable/function/BuiltinFunctionProviderGenerator.kt b/client/android/div-evaluable/src/generator/java/com/yandex/div/evaluable/function/BuiltinFunctionProviderGenerator.kt index e37bbaacf..630b3280a 100644 --- a/client/android/div-evaluable/src/generator/java/com/yandex/div/evaluable/function/BuiltinFunctionProviderGenerator.kt +++ b/client/android/div-evaluable/src/generator/java/com/yandex/div/evaluable/function/BuiltinFunctionProviderGenerator.kt @@ -98,9 +98,10 @@ object BuiltinFunctionProviderGenerator { addStatement("return %T", function) endControlFlow() } + val capitalizedTypeDescription = type.description.replaceFirstChar { it.titlecase() } code.addStatement( "throw %N(%N, %N)", - FunSpec.builder("getFunctionArgumentsException").build(), + FunSpec.builder("get${capitalizedTypeDescription}ArgumentsException").build(), nameParameterSpec, argsParameterSpec ) diff --git a/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/EvaluableException.kt b/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/EvaluableException.kt index 3b69f7969..876009cf5 100644 --- a/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/EvaluableException.kt +++ b/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/EvaluableException.kt @@ -70,7 +70,7 @@ internal fun functionToMessageFormat(name: String, args: List): String { internal fun methodToMessageFormat(name: String, args: List): String { return if (args.size > 1) { - args.subList(1, args.size).joinToString(prefix = "${args.first()}.$name(", postfix = ")", separator = ",") + args.subList(1, args.size).joinToString(prefix = "$name(", postfix = ")", separator = ",") } else "$name()" } diff --git a/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/function/ArrayFunctions.kt b/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/function/ArrayFunctions.kt index d3cad5bcc..a7233718e 100644 --- a/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/function/ArrayFunctions.kt +++ b/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/function/ArrayFunctions.kt @@ -27,6 +27,8 @@ internal abstract class ArrayFunction( ) override val isPure: Boolean = false + + open val isMethod: Boolean = false } internal abstract class ArrayOptFunction( @@ -48,7 +50,7 @@ internal abstract class ArrayInteger : ArrayFunction(EvaluableType.INTEGER) { expressionContext: ExpressionContext, args: List ): Any { - return when (val result = evaluate(name, args)) { + return when (val result = evaluateArray(name, args, isMethod)) { is Int -> result.toLong() is Long -> result is BigInteger -> throwArrayException(name, args, "Integer overflow.") @@ -61,7 +63,7 @@ internal abstract class ArrayInteger : ArrayFunction(EvaluableType.INTEGER) { if (result - longResult == 0.0) return longResult throwArrayException(name, args, "Cannot convert value to integer.") } - else -> throwWrongTypeException(name, args, resultType, result) + else -> throwArrayWrongTypeException(name, args, resultType, result, isMethod) } } } @@ -74,6 +76,11 @@ internal object GetIntegerFromArray : ArrayInteger() { override val name: String = "getIntegerFromArray" } +internal object ArrayGetInteger : ArrayInteger() { + override val name: String = "getInteger" + override val isMethod: Boolean = true +} + internal abstract class ArrayOptInteger : ArrayOptFunction(EvaluableType.INTEGER) { override fun evaluate( evaluationContext: EvaluationContext, @@ -102,8 +109,8 @@ internal abstract class ArrayNumber : ArrayFunction(EvaluableType.NUMBER) { expressionContext: ExpressionContext, args: List ): Any { - val result = evaluate(name, args) - return (result as? Number)?.toDouble() ?: throwWrongTypeException(name, args, resultType, result) + val result = evaluateArray(name, args, isMethod) + return (result as? Number)?.toDouble() ?: throwArrayWrongTypeException(name, args, resultType, result, isMethod) } } @@ -115,6 +122,11 @@ internal object GetNumberFromArray : ArrayNumber() { override val name: String = "getNumberFromArray" } +internal object ArrayGetNumber : ArrayNumber() { + override val name: String = "getNumber" + override val isMethod: Boolean = true +} + internal abstract class ArrayOptNumber : ArrayOptFunction(EvaluableType.NUMBER) { override fun evaluate( evaluationContext: EvaluationContext, @@ -140,8 +152,8 @@ internal abstract class ArrayString : ArrayFunction(EvaluableType.STRING) { expressionContext: ExpressionContext, args: List ): Any { - val result = evaluate(name, args) - return result as? String ?: throwWrongTypeException(name, args, resultType, result) + val result = evaluateArray(name, args, isMethod) + return result as? String ?: throwArrayWrongTypeException(name, args, resultType, result, isMethod) } } @@ -153,6 +165,11 @@ internal object GetStringFromArray : ArrayString() { override val name: String = "getStringFromArray" } +internal object ArrayGetString : ArrayString() { + override val name: String = "getString" + override val isMethod: Boolean = true +} + internal abstract class ArrayOptString : ArrayOptFunction(EvaluableType.STRING) { override fun evaluate( evaluationContext: EvaluationContext, @@ -175,9 +192,9 @@ internal abstract class ArrayColor : ArrayFunction(EvaluableType.COLOR) { expressionContext: ExpressionContext, args: List ): Any { - return when (val result = evaluate(name, args)) { + return when (val result = evaluateArray(name, args, isMethod)) { is Color -> result - !is String -> throwWrongTypeException(name, args, resultType, result) + !is String -> throwArrayWrongTypeException(name, args, resultType, result, isMethod) else -> { runCatching { Color.parse(result) @@ -197,6 +214,11 @@ internal object GetColorFromArray : ArrayColor() { override val name: String = "getColorFromArray" } +internal object ArrayGetColor : ArrayColor() { + override val name: String = "getColor" + override val isMethod: Boolean = true +} + internal abstract class ArrayOptColorWithStringFallback : ArrayOptFunction(EvaluableType.COLOR) { override val declaredArgs: List = listOf( @@ -253,8 +275,8 @@ internal abstract class ArrayUrl : ArrayFunction(EvaluableType.URL) { expressionContext: ExpressionContext, args: List ): Any { - val result = evaluate(name, args) - return (result as? String)?.safeConvertToUrl() ?: throwWrongTypeException(name, args, resultType, result) + val result = evaluateArray(name, args, isMethod) + return (result as? String)?.safeConvertToUrl() ?: throwArrayWrongTypeException(name, args, resultType, result, isMethod) } } @@ -266,6 +288,11 @@ internal object GetUrlFromArray : ArrayUrl() { override val name: String = "getUrlFromArray" } +internal object ArrayGetUrl : ArrayUrl() { + override val name: String = "getUrl" + override val isMethod: Boolean = true +} + internal abstract class ArrayOptUrlWithStringFallback : ArrayOptFunction(EvaluableType.URL) { override val declaredArgs: List = listOf( @@ -317,8 +344,8 @@ internal abstract class ArrayBoolean : ArrayFunction(EvaluableType.BOOLEAN) { expressionContext: ExpressionContext, args: List ): Any { - val result = evaluate(name, args) - return result as? Boolean ?: throwWrongTypeException(name, args, resultType, result) + val result = evaluateArray(name, args, isMethod) + return result as? Boolean ?: throwArrayWrongTypeException(name, args, resultType, result, isMethod) } } @@ -330,6 +357,11 @@ internal object GetBooleanFromArray : ArrayBoolean() { override val name: String = "getBooleanFromArray" } +internal object ArrayGetBoolean : ArrayBoolean() { + override val name: String = "getBoolean" + override val isMethod: Boolean = true +} + internal abstract class ArrayOptBoolean : ArrayOptFunction(EvaluableType.BOOLEAN) { override fun evaluate( evaluationContext: EvaluationContext, @@ -346,20 +378,26 @@ internal object GetOptBooleanFromArray : ArrayOptBoolean() { override val name: String = "getOptBooleanFromArray" } -internal object GetArrayFromArray : ArrayFunction(EvaluableType.ARRAY) { - - override val name: String = "getArrayFromArray" - +internal abstract class ArrayFromArray : ArrayFunction(EvaluableType.ARRAY) { override fun evaluate( evaluationContext: EvaluationContext, expressionContext: ExpressionContext, args: List ): Any { - val result = evaluate(name, args) - return result as? JSONArray ?: throwWrongTypeException(name, args, resultType, result) + val result = evaluateArray(name, args, isMethod) + return result as? JSONArray ?: throwArrayWrongTypeException(name, args, resultType, result, isMethod) } } +internal object GetArrayFromArray : ArrayFromArray() { + override val name: String = "getArrayFromArray" +} + +internal object ArrayGetArray : ArrayFromArray() { + override val name: String = "getArray" + override val isMethod: Boolean = true +} + internal object GetOptArrayFromArray : ArrayOptFunction(EvaluableType.ARRAY) { override val name: String = "getOptArrayFromArray" @@ -376,20 +414,26 @@ internal object GetOptArrayFromArray : ArrayOptFunction(EvaluableType.ARRAY) { ): Any = evaluateSafe(name, args) as? JSONArray ?: JSONArray() } -internal object GetDictFromArray : ArrayFunction(EvaluableType.DICT) { - - override val name: String = "getDictFromArray" - +internal abstract class ArrayDict : ArrayFunction(EvaluableType.DICT) { override fun evaluate( evaluationContext: EvaluationContext, expressionContext: ExpressionContext, args: List ): Any { - val result = evaluate(name, args) - return result as? JSONObject ?: throwWrongTypeException(name, args, resultType, result) + val result = evaluateArray(name, args, isMethod) + return result as? JSONObject ?: throwArrayWrongTypeException(name, args, resultType, result, isMethod) } } +internal object GetDictFromArray : ArrayDict() { + override val name: String = "getDictFromArray" +} + +internal object ArrayGetDict : ArrayDict() { + override val name: String = "getDict" + override val isMethod: Boolean = true +} + internal object GetOptDictFromArray : ArrayFunction(EvaluableType.DICT) { override val name: String = "getOptDictFromArray" @@ -403,7 +447,7 @@ internal object GetOptDictFromArray : ArrayFunction(EvaluableType.DICT) { evaluationContext: EvaluationContext, expressionContext: ExpressionContext, args: List - ): Any = evaluate(name, args) as? JSONObject ?: JSONObject() + ): Any = evaluateArray(name, args) as? JSONObject ?: JSONObject() } internal object GetArrayLength : Function() { @@ -428,8 +472,29 @@ internal object GetArrayLength : Function() { } } -internal fun evaluate(functionName: String, args: List): Any { - checkIndexOfBoundException(functionName, args) +internal object ArrayIsEmpty : Function() { + override val name: String = "isEmpty" + + override val declaredArgs: List = listOf( + FunctionArgument(type = EvaluableType.ARRAY) + ) + + override val resultType: EvaluableType = EvaluableType.BOOLEAN + + override val isPure: Boolean = false + + override fun evaluate( + evaluationContext: EvaluationContext, + expressionContext: ExpressionContext, + args: List + ): Any { + val array = args[0] as JSONArray + return array.length() == 0 + } +} + +internal fun evaluateArray(functionName: String, args: List, isMethod: Boolean = false): Any { + checkIndexOfBoundException(functionName, args, isMethod) val array = args[0] as JSONArray val index = args[1] as Long return array.get(index.toInt()) @@ -444,23 +509,25 @@ internal fun evaluateSafe(functionName: String, args: List): Any? { }.getOrNull() } -private fun checkIndexOfBoundException(functionName: String, args: List) { +private fun checkIndexOfBoundException(functionName: String, args: List, isMethod: Boolean = false) { val arraySize = (args[0] as JSONArray).length() val index = args[1] as Long if (index >= arraySize) { throwArrayException( functionName = functionName, args = args, - message = "Requested index (${index}) out of bounds array size ($arraySize)." + message = "Requested index (${index}) out of bounds array size ($arraySize).", + isMethod = isMethod ) } } -internal fun throwWrongTypeException( +internal fun throwArrayWrongTypeException( functionName: String, args: List, expected: EvaluableType, - actual: Any + actual: Any, + isMethod: Boolean = false ) { val actualType = when (actual) { JSONObject.NULL -> "Null" @@ -470,15 +537,16 @@ internal fun throwWrongTypeException( else -> actual.javaClass.simpleName } throwArrayException(functionName, args, - "Incorrect value type: expected ${expected.typeName}, got $actualType.") + "Incorrect value type: expected ${expected.typeName}, got $actualType.", isMethod) } -internal fun throwArrayException(functionName: String, args: List, message: String): Nothing = - throwException("array", functionName, args, message) +internal fun throwArrayException(functionName: String, args: List, message: String, isMethod: Boolean = false): Nothing = + throwException("array", functionName, args, message, isMethod) -internal fun throwException(type: String, functionName: String, args: List, message: String): Nothing { +internal fun throwException(type: String, functionName: String, args: List, message: String, isMethod: Boolean = false): Nothing { + val prefix = if (isMethod) "" else "<$type>, " val signature = args.subList(1, args.size) - .joinToString(prefix = "${functionName}(<$type>, ", postfix = ")") { + .joinToString(prefix = "${functionName}($prefix", postfix = ")") { it.toMessageFormat() } throwExceptionOnEvaluationFailed(signature, message) diff --git a/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/function/BuiltinFunctionProvider.kt b/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/function/BuiltinFunctionProvider.kt index d5ea1de2b..580887c1b 100644 --- a/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/function/BuiltinFunctionProvider.kt +++ b/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/function/BuiltinFunctionProvider.kt @@ -273,6 +273,15 @@ object BuiltinFunctionProvider : FunctionProvider { registry.register(GetOptArrayFromArray) registry.register(GetOptDictFromArray) registry.register(GetArrayLength) + registry.registerMethod(ArrayGetArray) + registry.registerMethod(ArrayGetBoolean) + registry.registerMethod(ArrayGetColor) + registry.registerMethod(ArrayGetDict) + registry.registerMethod(ArrayGetInteger) + registry.registerMethod(ArrayGetNumber) + registry.registerMethod(ArrayGetString) + registry.registerMethod(ArrayGetUrl) + registry.registerMethod(ArrayIsEmpty) } override fun get(name: String, args: List): Function { diff --git a/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/function/FunctionRegistry.kt b/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/function/FunctionRegistry.kt index e04ea64a9..92af6ce36 100644 --- a/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/function/FunctionRegistry.kt +++ b/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/function/FunctionRegistry.kt @@ -82,6 +82,6 @@ internal class FunctionRegistry : FunctionProvider { return overloaded.singleOrNull { it.matchesArgumentsWithCast(args) == Function.MatchResult.Ok - } ?: throw getFunctionArgumentsException(name, args) + } ?: throw getFunctionArgumentsException(name, args, isMethod) } } diff --git a/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/function/FunctionValidator.kt b/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/function/FunctionValidator.kt index fede025e8..6158375bc 100644 --- a/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/function/FunctionValidator.kt +++ b/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/function/FunctionValidator.kt @@ -83,9 +83,19 @@ internal fun Function.withArgumentsValidation(args: List): Functi } } -internal fun getFunctionArgumentsException(name: String, args: List): Exception { +internal fun getFunctionArgumentsException(name: String, args: List, isMethod: Boolean = false): Exception { + if (isMethod) { + return getMethodArgumentsException(name, args) + } if (args.isEmpty()) { return EvaluableException("Function requires non empty argument list.") } return EvaluableException("Function has no matching overload for given argument types: ${args.toMessageFormat()}.") } + +internal fun getMethodArgumentsException(name: String, args: List): Exception { + if (args.size == 1) { + return EvaluableException("Method requires non empty argument list.") + } + return EvaluableException("Method has no matching overload for given argument types: ${args.subList(1, args.size).toMessageFormat()}.") +} diff --git a/test_data/expression_test_data/methods_array.json b/test_data/expression_test_data/methods_array.json index 14307b0a2..3f6863ecb 100644 --- a/test_data/expression_test_data/methods_array.json +++ b/test_data/expression_test_data/methods_array.json @@ -24,7 +24,8 @@ ], "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -45,7 +46,8 @@ ], "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -66,7 +68,8 @@ ], "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -93,7 +96,8 @@ ], "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -114,7 +118,8 @@ ], "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -135,7 +140,8 @@ ], "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -156,7 +162,8 @@ ], "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -177,7 +184,8 @@ ], "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -197,7 +205,8 @@ ], "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -217,7 +226,8 @@ ], "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -238,7 +248,8 @@ ], "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -259,7 +270,8 @@ ], "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -277,7 +289,8 @@ ], "platforms": [ "ios", - "web" + "web", + "android" ] } ] diff --git a/test_data/expression_test_data/methods_dict.json b/test_data/expression_test_data/methods_dict.json index 73bd7c769..793089cf7 100644 --- a/test_data/expression_test_data/methods_dict.json +++ b/test_data/expression_test_data/methods_dict.json @@ -342,7 +342,8 @@ ], "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -360,7 +361,8 @@ ], "platforms": [ "ios", - "web" + "web", + "android" ] }, { diff --git a/test_data/expression_test_data/methods_signatures_array.json b/test_data/expression_test_data/methods_signatures_array.json index 7743de107..a251ec020 100644 --- a/test_data/expression_test_data/methods_signatures_array.json +++ b/test_data/expression_test_data/methods_signatures_array.json @@ -17,7 +17,8 @@ "result_type": "array", "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -37,7 +38,8 @@ "result_type": "boolean", "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -57,7 +59,8 @@ "result_type": "color", "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -77,7 +80,8 @@ "result_type": "dict", "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -97,7 +101,8 @@ "result_type": "integer", "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -117,7 +122,8 @@ "result_type": "number", "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -137,7 +143,8 @@ "result_type": "string", "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -157,7 +164,8 @@ "result_type": "url", "platforms": [ "ios", - "web" + "web", + "android" ] }, { @@ -173,7 +181,8 @@ "result_type": "boolean", "platforms": [ "ios", - "web" + "web", + "android" ] } ]