diff --git a/.mapping.json b/.mapping.json index 8be203a93..350c0a378 100644 --- a/.mapping.json +++ b/.mapping.json @@ -734,6 +734,7 @@ "client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/Function.kt":"divkit/public/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/Function.kt", "client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/FunctionArgument.kt":"divkit/public/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/FunctionArgument.kt", "client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/FunctionProvider.kt":"divkit/public/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/FunctionProvider.kt", + "client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/LocalFunctionProvider.kt":"divkit/public/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/LocalFunctionProvider.kt", "client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/StoredValueProvider.kt":"divkit/public/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/StoredValueProvider.kt", "client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/VariableProvider.kt":"divkit/public/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/VariableProvider.kt", "client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/WarningSender.kt":"divkit/public/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/WarningSender.kt", @@ -1078,23 +1079,27 @@ "client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionResolverImpl.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionResolverImpl.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionsRuntime.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionsRuntime.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionsRuntimeProvider.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionsRuntimeProvider.kt", + "client/android/div/src/main/java/com/yandex/div/core/expression/FunctionProviderDecorator.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/FunctionProviderDecorator.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/local/ChildPathUnitCache.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/local/ChildPathUnitCache.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/local/DivRuntimeVisitor.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/local/DivRuntimeVisitor.kt", + "client/android/div/src/main/java/com/yandex/div/core/expression/local/LocalFunction.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/local/LocalFunction.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/local/RuntimeStore.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/local/RuntimeStore.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/local/RuntimeTree.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/local/RuntimeTree.kt", + "client/android/div/src/main/java/com/yandex/div/core/expression/local/utils.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/local/utils.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/storedvalues/StoredValueDeclarationException.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/storedvalues/StoredValueDeclarationException.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/storedvalues/StoredValuesActionHandler.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/storedvalues/StoredValuesActionHandler.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/storedvalues/StoredValuesController.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/storedvalues/StoredValuesController.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/triggers/ConditionPart.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/triggers/ConditionPart.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/triggers/TriggersController.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/triggers/TriggersController.kt", + "client/android/div/src/main/java/com/yandex/div/core/expression/variables/ConstantsProvider.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/variables/ConstantsProvider.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/variables/DivVariableController.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/variables/DivVariableController.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/variables/DivVariablesParser.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/variables/DivVariablesParser.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/variables/GlobalVariableController.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/variables/GlobalVariableController.kt", - "client/android/div/src/main/java/com/yandex/div/core/expression/variables/LocalVariableController.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/variables/LocalVariableController.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/variables/MultiVariableSource.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/variables/MultiVariableSource.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/variables/Observers.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/variables/Observers.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/variables/SingleVariableSource.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/variables/SingleVariableSource.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/variables/TwoWayVariableBinder.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/variables/TwoWayVariableBinder.kt", + "client/android/div/src/main/java/com/yandex/div/core/expression/variables/VariableAndConstantController.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/variables/VariableAndConstantController.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/variables/VariableController.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/variables/VariableController.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/variables/VariableControllerImpl.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/variables/VariableControllerImpl.kt", "client/android/div/src/main/java/com/yandex/div/core/expression/variables/VariableDeclarationNotifier.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/expression/variables/VariableDeclarationNotifier.kt", @@ -1147,6 +1152,7 @@ "client/android/div/src/main/java/com/yandex/div/core/util/DivTreeWalk.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/DivTreeWalk.kt", "client/android/div/src/main/java/com/yandex/div/core/util/DivUtil.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/DivUtil.kt", "client/android/div/src/main/java/com/yandex/div/core/util/ExpressionSubscribers.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/ExpressionSubscribers.kt", + "client/android/div/src/main/java/com/yandex/div/core/util/FunctionMapper.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/FunctionMapper.kt", "client/android/div/src/main/java/com/yandex/div/core/util/ImageRepresentation.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/ImageRepresentation.kt", "client/android/div/src/main/java/com/yandex/div/core/util/ImageUtils.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/ImageUtils.kt", "client/android/div/src/main/java/com/yandex/div/core/util/Releasables.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/Releasables.kt", 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 cc6e1c7f5..3b69f7969 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 @@ -36,6 +36,11 @@ class IntegerOverflow( cause: Exception? = null ) : EvaluableException("Failed to evaluate [$expression]. $REASON_INTEGER_OVERFLOW", cause) +class MissingLocalFunctionException( + name: String, + args: List, +): EvaluableException("Function '$name(${args.toMessageFormat()})' is missing.") + internal fun throwExceptionOnEvaluationFailed( expression: String, reason: String, diff --git a/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/Function.kt b/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/Function.kt index bf9dadde9..67a314d61 100644 --- a/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/Function.kt +++ b/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/Function.kt @@ -20,7 +20,9 @@ abstract class Function { ): Any { val result = evaluate(evaluationContext, expressionContext, args) if (EvaluableType.of(result) != resultType) { - throw EvaluableException("Function returned ${EvaluableType.of(result)}, but $resultType was expected") + throw EvaluableException( + "Function $this returned ${EvaluableType.of(result)}, but $resultType was expected." + ) } return result } diff --git a/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/LocalFunctionProvider.kt b/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/LocalFunctionProvider.kt new file mode 100644 index 000000000..bdebce74f --- /dev/null +++ b/client/android/div-evaluable/src/main/java/com/yandex/div/evaluable/LocalFunctionProvider.kt @@ -0,0 +1,25 @@ +package com.yandex.div.evaluable + +class LocalFunctionProvider(private val functions: List) : FunctionProvider { + + override fun get(name: String, args: List): Function { + findFunction(name) { matchesArguments(args) }?.let { return it } + findFunction(name) { matchesArgumentsWithCast(args) }?.let { return it } + throw MissingLocalFunctionException(name, args) + } + + override fun getMethod(name: String, args: List): Function { + findFunction(name) { matchesArguments(args) }?.let { return it } + findFunction(name) { matchesArgumentsWithCast(args) }?.let { return it } + throw MissingLocalFunctionException(name, args) + } + + private fun findFunction(name: String, matcher: Function.() -> Function.MatchResult): Function? { + val localFunctions = functions.filter { it.name == name && it.matcher() == Function.MatchResult.Ok } + return when (localFunctions.size) { + 0 -> null + 1 -> return localFunctions[0] + else -> throw EvaluableException("Function ${localFunctions[0]} declared multiple times.") + } + } +} diff --git a/client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionResolverImpl.kt b/client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionResolverImpl.kt index 86aaa1bb2..d14a4f378 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionResolverImpl.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionResolverImpl.kt @@ -2,9 +2,9 @@ package com.yandex.div.core.expression import com.yandex.div.core.Disposable import com.yandex.div.core.ObserverList -import com.yandex.div.core.expression.variables.LocalVariableController +import com.yandex.div.core.expression.variables.ConstantsProvider +import com.yandex.div.core.expression.variables.VariableAndConstantController import com.yandex.div.core.expression.variables.VariableController -import com.yandex.div.core.expression.variables.VariableSource import com.yandex.div.core.view2.errors.ErrorCollector import com.yandex.div.evaluable.Evaluable import com.yandex.div.evaluable.EvaluableException @@ -40,7 +40,11 @@ internal class ExpressionResolverImpl( var suppressMissingVariableException: Boolean = false init { - onCreateCallback.onCreate(this, variableController) + onCreateCallback.onCreate( + this, + variableController, + evaluator.evaluationContext.functionProvider as FunctionProviderDecorator + ) } override fun get( @@ -226,13 +230,13 @@ internal class ExpressionResolverImpl( } } - operator fun plus(variableSource: VariableSource): ExpressionResolverImpl { - val localVariableController = LocalVariableController(variableController, variableSource) + operator fun plus(constants: ConstantsProvider): ExpressionResolverImpl { + val variableAndConstantController = VariableAndConstantController(variableController, constants) return ExpressionResolverImpl( - variableController = localVariableController, + variableController = variableAndConstantController, evaluator = Evaluator( evaluationContext = EvaluationContext( - variableProvider = localVariableController, + variableProvider = variableAndConstantController, storedValueProvider = evaluator.evaluationContext.storedValueProvider, functionProvider = evaluator.evaluationContext.functionProvider, warningSender = evaluator.evaluationContext.warningSender @@ -256,6 +260,10 @@ internal class ExpressionResolverImpl( * as a new ExpressionRuntime we are using OnCreateCallback. */ internal fun interface OnCreateCallback { - fun onCreate(resolver: ExpressionResolverImpl, variableController: VariableController) + fun onCreate( + resolver: ExpressionResolverImpl, + variableController: VariableController, + functionProvider: FunctionProviderDecorator + ) } } diff --git a/client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionsRuntime.kt b/client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionsRuntime.kt index c4dc26527..1c83bc22c 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionsRuntime.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionsRuntime.kt @@ -11,6 +11,7 @@ internal class ExpressionsRuntime( val expressionResolver: ExpressionResolver, val variableController: VariableController, val triggersController: TriggersController? = null, + val functionProvider: FunctionProviderDecorator, val runtimeStore: RuntimeStore, ) { private val expressionResolverImpl get() = expressionResolver as? ExpressionResolverImpl diff --git a/client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionsRuntimeProvider.kt b/client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionsRuntimeProvider.kt index 26a002fda..dfb8b0171 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionsRuntimeProvider.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/expression/ExpressionsRuntimeProvider.kt @@ -125,12 +125,13 @@ internal class ExpressionsRuntimeProvider @Inject constructor( addSource(globalVariableController.variableSource) } + val functionProvider = FunctionProviderDecorator(GeneratedBuiltinFunctionProvider) val evaluationContext = EvaluationContext( variableProvider = variableController, storedValueProvider = { storedValueName -> storedValuesController.getStoredValue(storedValueName, errorCollector)?.getValue() }, - functionProvider = GeneratedBuiltinFunctionProvider, + functionProvider = functionProvider, warningSender = { expressionContext, message -> val rawExpr = expressionContext.evaluable.rawExpr val warning = "Warning occurred while evaluating '$rawExpr': $message" @@ -142,9 +143,9 @@ internal class ExpressionsRuntimeProvider @Inject constructor( val evaluator = Evaluator(evaluationContext) val runtimeStore = RuntimeStore(evaluator, errorCollector, logger, divActionBinder) - val callback = ExpressionResolverImpl.OnCreateCallback { resolver, variableController -> + val callback = ExpressionResolverImpl.OnCreateCallback { resolver, variableController, functionProvider -> runtimeStore.putRuntime( - runtime = ExpressionsRuntime(resolver, variableController, null, runtimeStore) + runtime = ExpressionsRuntime(resolver, variableController, null, functionProvider, runtimeStore) ) } @@ -168,6 +169,7 @@ internal class ExpressionsRuntimeProvider @Inject constructor( expressionResolver, variableController, triggersController, + functionProvider, runtimeStore, ).also { runtimeStore.rootRuntime = it } } diff --git a/client/android/div/src/main/java/com/yandex/div/core/expression/FunctionProviderDecorator.kt b/client/android/div/src/main/java/com/yandex/div/core/expression/FunctionProviderDecorator.kt new file mode 100644 index 000000000..0be3f5894 --- /dev/null +++ b/client/android/div/src/main/java/com/yandex/div/core/expression/FunctionProviderDecorator.kt @@ -0,0 +1,38 @@ +package com.yandex.div.core.expression + +import com.yandex.div.evaluable.EvaluableType +import com.yandex.div.evaluable.Function +import com.yandex.div.evaluable.FunctionProvider +import com.yandex.div.evaluable.LocalFunctionProvider +import com.yandex.div.evaluable.MissingLocalFunctionException + +internal class FunctionProviderDecorator(private val provider: FunctionProvider) : FunctionProvider { + + override fun get(name: String, args: List) = provider.get(name, args) + + override fun getMethod(name: String, args: List) = provider.getMethod(name, args) + + operator fun plus(functions: List): FunctionProviderDecorator { + val localProvider = LocalFunctionProvider(functions) + return FunctionProviderDecorator( + object : FunctionProvider { + + override fun get(name: String, args: List): Function { + return try { + localProvider.get(name, args) + } catch (e: MissingLocalFunctionException) { + provider.get(name, args) + } + } + + override fun getMethod(name: String, args: List): Function { + return try { + localProvider.getMethod(name, args) + } catch (e: MissingLocalFunctionException) { + provider.getMethod(name, args) + } + } + } + ) + } +} diff --git a/client/android/div/src/main/java/com/yandex/div/core/expression/local/DivRuntimeVisitor.kt b/client/android/div/src/main/java/com/yandex/div/core/expression/local/DivRuntimeVisitor.kt index 5df425261..9fea1c449 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/expression/local/DivRuntimeVisitor.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/expression/local/DivRuntimeVisitor.kt @@ -123,19 +123,20 @@ internal class DivRuntimeVisitor @Inject constructor( path: MutableList, parentRuntime: ExpressionsRuntime? ): ExpressionsRuntime? { - return if (div.value().variables.isNullOrEmpty() && div.value().variableTriggers.isNullOrEmpty()) { - parentRuntime - } else { + return if (div.needLocalRuntime) { val stringPath = path.joinToString("/") divView.runtimeStore?.getOrCreateRuntime( path = stringPath, variables = div.value().variables?.toVariables(), triggers = div.value().variableTriggers, + functions = div.value().functions, parentRuntime = parentRuntime, )?.also { runtime -> runtime.onAttachedToWindow(divView) } ?: run { Assert.fail("ExpressionRuntimeVisitor cannot create runtime for path = $stringPath") null } + } else { + parentRuntime } } diff --git a/client/android/div/src/main/java/com/yandex/div/core/expression/local/LocalFunction.kt b/client/android/div/src/main/java/com/yandex/div/core/expression/local/LocalFunction.kt new file mode 100644 index 000000000..865c9d55e --- /dev/null +++ b/client/android/div/src/main/java/com/yandex/div/core/expression/local/LocalFunction.kt @@ -0,0 +1,47 @@ +package com.yandex.div.core.expression.local + +import com.yandex.div.core.expression.variables.ConstantsProvider +import com.yandex.div.core.expression.variables.VariableAndConstantController +import com.yandex.div.core.expression.variables.VariableController +import com.yandex.div.evaluable.Evaluable +import com.yandex.div.evaluable.EvaluableType +import com.yandex.div.evaluable.EvaluationContext +import com.yandex.div.evaluable.Evaluator +import com.yandex.div.evaluable.ExpressionContext +import com.yandex.div.evaluable.Function +import com.yandex.div.evaluable.FunctionArgument + +class LocalFunction( + override val name: String, + override val declaredArgs: List, + override val resultType: EvaluableType, + private val argNames: List, + body: String, +) : Function() { + + override val isPure = false + + private val evaluable = Evaluable.lazy(body) + + override fun evaluate( + evaluationContext: EvaluationContext, + expressionContext: ExpressionContext, + args: List + ): Any { + val argConstants = mutableMapOf() + argNames.forEachIndexed { index, name -> + argConstants[name] = args[index] + } + val variableProvider = VariableAndConstantController( + evaluationContext.variableProvider as VariableController, + ConstantsProvider(argConstants), + ) + val newContext = EvaluationContext( + variableProvider, + evaluationContext.storedValueProvider, + evaluationContext.functionProvider, + evaluationContext.warningSender + ) + return Evaluator(newContext).eval(evaluable) + } +} diff --git a/client/android/div/src/main/java/com/yandex/div/core/expression/local/RuntimeStore.kt b/client/android/div/src/main/java/com/yandex/div/core/expression/local/RuntimeStore.kt index 9467de862..acf32a6d1 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/expression/local/RuntimeStore.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/expression/local/RuntimeStore.kt @@ -5,6 +5,7 @@ import com.yandex.div.core.expression.ExpressionResolverImpl import com.yandex.div.core.expression.ExpressionsRuntime import com.yandex.div.core.expression.triggers.TriggersController import com.yandex.div.core.expression.variables.VariableControllerImpl +import com.yandex.div.core.util.toLocalFunctions import com.yandex.div.core.view2.divs.DivActionBinder import com.yandex.div.core.view2.errors.ErrorCollector import com.yandex.div.data.Variable @@ -13,6 +14,7 @@ import com.yandex.div.evaluable.Evaluator import com.yandex.div.internal.Assert import com.yandex.div.json.expressions.ExpressionResolver import com.yandex.div2.DivBase +import com.yandex.div2.DivFunction import com.yandex.div2.DivTrigger private const val ERROR_UNKNOWN_RESOLVER = @@ -41,8 +43,8 @@ internal class RuntimeStore( } private val onCreateCallback by lazy { - ExpressionResolverImpl.OnCreateCallback { resolver, variableController -> - ExpressionsRuntime(resolver, variableController, null, this).also { + ExpressionResolverImpl.OnCreateCallback { resolver, variableController, functionProvider -> + ExpressionsRuntime(resolver, variableController, null, functionProvider, this).also { /** * we cannot provide path here, otherwise descendants of ExpressionResolver will * receive the same callback and override runtime for provided path. @@ -70,10 +72,11 @@ internal class RuntimeStore( path: String, variables: List? = null, triggers: List? = null, + functions: List? = null, parentResolver: ExpressionResolver? = null, parentRuntime: ExpressionsRuntime? = null, ) = tree.getNode(path)?.runtime ?: getRuntimeOrCreateChild( - path, variables, triggers,null, parentResolver, parentRuntime + path, variables, triggers, functions, null, parentResolver, parentRuntime ) internal fun getRuntimeWithOrNull(resolver: ExpressionResolver) = resolverToRuntime[resolver] @@ -98,6 +101,7 @@ internal class RuntimeStore( path: String, variables: List?, triggers: List?, + functions: List?, resolver: ExpressionResolver, parentResolver: ExpressionResolver?, ): ExpressionsRuntime? { @@ -110,7 +114,7 @@ internal class RuntimeStore( } if (runtimeForPath != null) tree.removeRuntimeAndCleanup(runtimeForPath, path) - return getRuntimeOrCreateChild(path, variables, triggers, existingRuntime, parentResolver) + return getRuntimeOrCreateChild(path, variables, triggers, functions, existingRuntime, parentResolver) } internal fun cleanup() { @@ -135,14 +139,18 @@ internal class RuntimeStore( path: String, variables: List?, variablesTriggers: List?, + functions: List?, ): ExpressionsRuntime { val localVariableController = VariableControllerImpl(baseRuntime.variableController) variables?.forEach { localVariableController.declare(it) } + var functionProvider = baseRuntime.functionProvider + functions?.let { functionProvider += it.toLocalFunctions() } + val evaluationContext = EvaluationContext( variableProvider = localVariableController, storedValueProvider = evaluator.evaluationContext.storedValueProvider, - functionProvider = evaluator.evaluationContext.functionProvider, + functionProvider = functionProvider, warningSender = evaluator.evaluationContext.warningSender ) @@ -165,7 +173,7 @@ internal class RuntimeStore( ensureTriggersSynced(variablesTriggers) } - return ExpressionsRuntime(resolver, localVariableController, triggerController, this).also { + return ExpressionsRuntime(resolver, localVariableController, triggerController, functionProvider, this).also { putRuntime(it, path, parentRuntime) } } @@ -174,6 +182,7 @@ internal class RuntimeStore( path: String, variables: List?, variablesTriggers: List?, + functions: List?, existingRuntime: ExpressionsRuntime? = null, parentResolver: ExpressionResolver? = null, parentRuntime: ExpressionsRuntime? = null, @@ -189,13 +198,13 @@ internal class RuntimeStore( val parentRuntime = parentRuntime ?: parentResolver?.let { getRuntimeWithOrNull(it) } - return if (variables.isNullOrEmpty() && variablesTriggers.isNullOrEmpty()) { + return if (needLocalRuntime(variables, variablesTriggers, functions)) { + createChildRuntime(runtime, parentRuntime, path, variables, variablesTriggers, functions) + } else { runtime.also { tree.storeRuntime(runtime, parentRuntime, path) runtime.updateSubscriptions() } - } else { - createChildRuntime(runtime, parentRuntime, path, variables, variablesTriggers) } } } diff --git a/client/android/div/src/main/java/com/yandex/div/core/expression/local/RuntimeTree.kt b/client/android/div/src/main/java/com/yandex/div/core/expression/local/RuntimeTree.kt index 2006fc373..e2337a708 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/expression/local/RuntimeTree.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/expression/local/RuntimeTree.kt @@ -1,7 +1,7 @@ package com.yandex.div.core.expression.local import com.yandex.div.core.expression.ExpressionsRuntime -import com.yandex.div.core.expression.variables.LocalVariableController +import com.yandex.div.core.expression.variables.VariableAndConstantController internal class RuntimeTree { private val runtimesToNodes = mutableMapOf() @@ -49,7 +49,7 @@ internal class RuntimeTree { pathToNodes.remove(it.path) // we should not cleanup LocalVariableController because it will just cleanup it's delegate - if (it.runtime.variableController !is LocalVariableController) it.runtime.cleanup() + if (it.runtime.variableController !is VariableAndConstantController) it.runtime.cleanup() } } diff --git a/client/android/div/src/main/java/com/yandex/div/core/expression/local/utils.kt b/client/android/div/src/main/java/com/yandex/div/core/expression/local/utils.kt new file mode 100644 index 000000000..80e0ab60c --- /dev/null +++ b/client/android/div/src/main/java/com/yandex/div/core/expression/local/utils.kt @@ -0,0 +1,15 @@ +package com.yandex.div.core.expression.local + +import com.yandex.div.data.Variable +import com.yandex.div2.Div +import com.yandex.div2.DivFunction +import com.yandex.div2.DivTrigger + +internal val Div.needLocalRuntime get() = + !value().run { variables.isNullOrEmpty() && variableTriggers.isNullOrEmpty() && functions.isNullOrEmpty() } + +internal fun needLocalRuntime( + variables: List?, + variableTriggers: List?, + functions: List? +) = !(variables.isNullOrEmpty() && variableTriggers.isNullOrEmpty() && functions.isNullOrEmpty()) diff --git a/client/android/div/src/main/java/com/yandex/div/core/expression/variables/ConstantsProvider.kt b/client/android/div/src/main/java/com/yandex/div/core/expression/variables/ConstantsProvider.kt new file mode 100644 index 000000000..8a3707347 --- /dev/null +++ b/client/android/div/src/main/java/com/yandex/div/core/expression/variables/ConstantsProvider.kt @@ -0,0 +1,7 @@ +package com.yandex.div.core.expression.variables + +import com.yandex.div.evaluable.VariableProvider + +internal class ConstantsProvider(private val constants: Map) : VariableProvider { + override fun get(name: String) = constants[name] +} diff --git a/client/android/div/src/main/java/com/yandex/div/core/expression/variables/LocalVariableController.kt b/client/android/div/src/main/java/com/yandex/div/core/expression/variables/VariableAndConstantController.kt similarity index 84% rename from client/android/div/src/main/java/com/yandex/div/core/expression/variables/LocalVariableController.kt rename to client/android/div/src/main/java/com/yandex/div/core/expression/variables/VariableAndConstantController.kt index 56d03b02d..9c061c6d1 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/expression/variables/LocalVariableController.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/expression/variables/VariableAndConstantController.kt @@ -4,11 +4,13 @@ import com.yandex.div.core.Disposable import com.yandex.div.core.view2.errors.ErrorCollector import com.yandex.div.data.Variable -internal class LocalVariableController( +internal class VariableAndConstantController( private val delegate: VariableController, - private val localVariables: VariableSource + private val constants: ConstantsProvider, ) : VariableController { + override fun get(name: String) = constants.get(name) ?: super.get(name) + override fun subscribeToVariablesChange( names: List, invokeOnSubscription: Boolean, @@ -31,8 +33,7 @@ internal class LocalVariableController( override fun addSource(source: VariableSource) = delegate.addSource(source) - override fun getMutableVariable(name: String) = - localVariables.getMutableVariable(name) ?: delegate.getMutableVariable(name) + override fun getMutableVariable(name: String) = delegate.getMutableVariable(name) override fun declare(variable: Variable) = delegate.declare(variable) diff --git a/client/android/div/src/main/java/com/yandex/div/core/util/FunctionMapper.kt b/client/android/div/src/main/java/com/yandex/div/core/util/FunctionMapper.kt new file mode 100644 index 000000000..ae2fccf01 --- /dev/null +++ b/client/android/div/src/main/java/com/yandex/div/core/util/FunctionMapper.kt @@ -0,0 +1,37 @@ +package com.yandex.div.core.util + +import com.yandex.div.evaluable.EvaluableType +import com.yandex.div.evaluable.FunctionArgument +import com.yandex.div.core.expression.local.LocalFunction +import com.yandex.div2.DivEvaluableType +import com.yandex.div2.DivFunction + +internal fun List.toLocalFunctions(): List { + return map { divFunction -> + val argNames = mutableListOf() + val argTypes = mutableListOf() + divFunction.arguments.forEach { + argNames.add(it.name) + argTypes.add(FunctionArgument(it.type.toEvaluableType())) + } + LocalFunction( + divFunction.name, + argTypes, + divFunction.returnType.toEvaluableType(), + argNames, + divFunction.body, + ) + } +} + +private fun DivEvaluableType.toEvaluableType() = when (this) { + DivEvaluableType.STRING -> EvaluableType.STRING + DivEvaluableType.INTEGER -> EvaluableType.INTEGER + DivEvaluableType.NUMBER -> EvaluableType.NUMBER + DivEvaluableType.BOOLEAN -> EvaluableType.BOOLEAN + DivEvaluableType.DATETIME -> EvaluableType.DATETIME + DivEvaluableType.COLOR -> EvaluableType.COLOR + DivEvaluableType.URL -> EvaluableType.URL + DivEvaluableType.DICT -> EvaluableType.DICT + DivEvaluableType.ARRAY -> EvaluableType.ARRAY +} diff --git a/client/android/div/src/main/java/com/yandex/div/core/view2/DivBinder.kt b/client/android/div/src/main/java/com/yandex/div/core/view2/DivBinder.kt index f1b5412fb..ff570478c 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/view2/DivBinder.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/view2/DivBinder.kt @@ -5,6 +5,7 @@ import android.view.ViewGroup import androidx.annotation.MainThread import com.yandex.div.core.annotations.Mockable import com.yandex.div.core.dagger.DivScope +import com.yandex.div.core.expression.local.needLocalRuntime import com.yandex.div.core.expression.suppressExpressionErrors import com.yandex.div.core.extension.DivExtensionController import com.yandex.div.core.state.DivStatePath @@ -250,17 +251,18 @@ internal class DivBinder @Inject constructor( private fun getBindingContext( parentContext: BindingContext, div: Div, path: DivStatePath ): BindingContext { - return if (div.value().variableTriggers.isNullOrEmpty() && div.value().variables.isNullOrEmpty()) { - parentContext - } else { + return if (div.needLocalRuntime) { parentContext.getFor( parentContext.runtimeStore?.getOrCreateRuntime( path = path.fullPath, parentResolver = parentContext.expressionResolver, variables = div.value().variables?.toVariables(), triggers = div.value().variableTriggers, + functions = div.value().functions, )?.expressionResolver ?: parentContext.expressionResolver ) + } else { + parentContext } } } diff --git a/client/android/div/src/main/java/com/yandex/div/core/view2/divs/BaseDivViewExtensions.kt b/client/android/div/src/main/java/com/yandex/div/core/view2/divs/BaseDivViewExtensions.kt index bc6300b48..215f2cafd 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/view2/divs/BaseDivViewExtensions.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/view2/divs/BaseDivViewExtensions.kt @@ -630,12 +630,13 @@ internal fun resolveRuntime( resolver: ExpressionResolver, parentResolver: ExpressionResolver, ) = runtimeStore?.resolveRuntimeWith( - path = path, - variables = div.variables?.toVariables(), - triggers = div.variableTriggers, - resolver = resolver, - parentResolver = parentResolver, - ) + path = path, + variables = div.variables?.toVariables(), + triggers = div.variableTriggers, + functions = div.functions, + resolver = resolver, + parentResolver = parentResolver, +) internal fun DivBase.getChildPathUnit(index: Int) = id ?: ChildPathUnitCache.getValue(index) diff --git a/client/android/div/src/main/java/com/yandex/div/internal/core/DivCollectionExtensions.kt b/client/android/div/src/main/java/com/yandex/div/internal/core/DivCollectionExtensions.kt index 7ca7c9f3c..f14db3010 100644 --- a/client/android/div/src/main/java/com/yandex/div/internal/core/DivCollectionExtensions.kt +++ b/client/android/div/src/main/java/com/yandex/div/internal/core/DivCollectionExtensions.kt @@ -2,8 +2,7 @@ package com.yandex.div.internal.core import com.yandex.div.core.annotations.InternalApi import com.yandex.div.core.expression.ExpressionResolverImpl -import com.yandex.div.core.expression.variables.VariableSource -import com.yandex.div.data.Variable +import com.yandex.div.core.expression.variables.ConstantsProvider import com.yandex.div.internal.util.forEach import com.yandex.div.internal.util.mapIndexedNotNull import com.yandex.div.json.expressions.ExpressionResolver @@ -61,15 +60,11 @@ private fun DivCollectionItemBuilder.getItemResolver( ): ExpressionResolver? { val resolverImpl = resolver as? ExpressionResolverImpl ?: return resolver val validElement = resolverImpl.validateItemBuilderDataElement(dataElement, index) ?: return null - val localVariableSource = VariableSource( - mapOf( - dataElementName to Variable.DictVariable(dataElementName, validElement), - INDEX_VARIABLE_NAME to Variable.IntegerVariable(INDEX_VARIABLE_NAME, index.toLong()) - ), - {}, - mutableListOf() - ) - return resolverImpl + localVariableSource + val localDataProvider = ConstantsProvider(mapOf( + dataElementName to validElement, + INDEX_VARIABLE_NAME to index.toLong() + )) + return resolverImpl + localDataProvider } private fun Div.copy(id: String? = value().id): Div { diff --git a/client/android/div/src/test/java/com/yandex/div/core/expression/ExpressionResolverImplTest.kt b/client/android/div/src/test/java/com/yandex/div/core/expression/ExpressionResolverImplTest.kt index 29e2aaf7f..209f6e461 100644 --- a/client/android/div/src/test/java/com/yandex/div/core/expression/ExpressionResolverImplTest.kt +++ b/client/android/div/src/test/java/com/yandex/div/core/expression/ExpressionResolverImplTest.kt @@ -11,18 +11,24 @@ import com.yandex.div.evaluable.Function import com.yandex.div.evaluable.FunctionProvider import com.yandex.div.evaluable.function.GeneratedBuiltinFunctionProvider import com.yandex.div.evaluable.types.DateTime -import com.yandex.div.internal.parser.* +import com.yandex.div.internal.parser.Converter +import com.yandex.div.internal.parser.NUMBER_TO_DOUBLE +import com.yandex.div.internal.parser.TYPE_HELPER_BOOLEAN +import com.yandex.div.internal.parser.TYPE_HELPER_DOUBLE +import com.yandex.div.internal.parser.TYPE_HELPER_INT +import com.yandex.div.internal.parser.TYPE_HELPER_STRING +import com.yandex.div.internal.parser.TypeHelper import com.yandex.div.json.ParsingErrorLogger import com.yandex.div.json.ParsingException import com.yandex.div.json.expressions.Expression import org.junit.Assert +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.robolectric.RobolectricTestRunner -import java.util.* -import org.junit.Before +import java.util.TimeZone /** * Tests for [ExpressionResolverImpl]. @@ -70,7 +76,7 @@ class ExpressionResolverImplTest { private val evaluationContext = EvaluationContext( variableProvider = { externalVariables.getMutableVariable(it)?.getValue() }, storedValueProvider = mock(), - functionProvider = GeneratedBuiltinFunctionProvider, + functionProvider = FunctionProviderDecorator(GeneratedBuiltinFunctionProvider), warningSender = { _, _ -> } ) @@ -78,7 +84,7 @@ class ExpressionResolverImplTest { externalVariables, Evaluator(evaluationContext), mock() - ) { _, _ -> } + ) { _, _, _ -> } private val withFuncGetCallback = { callback: () -> Unit -> ExpressionResolverImpl( @@ -87,7 +93,7 @@ class ExpressionResolverImplTest { EvaluationContext( variableProvider = evaluationContext.variableProvider, storedValueProvider = evaluationContext.storedValueProvider, - functionProvider = object : FunctionProvider { + functionProvider = FunctionProviderDecorator(object : FunctionProvider { override fun get(name: String, args: List): Function { callback() return evaluationContext.functionProvider.get(name, args) @@ -97,12 +103,12 @@ class ExpressionResolverImplTest { callback() return evaluationContext.functionProvider.getMethod(name, args) } - }, + }), warningSender = evaluationContext.warningSender ) ), mock() - ) { _, _ -> } + ) { _, _, _ -> } } @Before diff --git a/client/android/div/src/test/java/com/yandex/div/core/expression/local/RuntimeStoreTest.kt b/client/android/div/src/test/java/com/yandex/div/core/expression/local/RuntimeStoreTest.kt index 71602c0ac..4ad6c922f 100644 --- a/client/android/div/src/test/java/com/yandex/div/core/expression/local/RuntimeStoreTest.kt +++ b/client/android/div/src/test/java/com/yandex/div/core/expression/local/RuntimeStoreTest.kt @@ -3,6 +3,7 @@ package com.yandex.div.core.expression.local import com.yandex.div.core.Div2Logger import com.yandex.div.core.expression.ExpressionResolverImpl import com.yandex.div.core.expression.ExpressionsRuntime +import com.yandex.div.core.expression.FunctionProviderDecorator import com.yandex.div.core.expression.variables.VariableController import com.yandex.div.core.state.DivStatePath import com.yandex.div.core.view2.divs.DivActionBinder @@ -26,7 +27,7 @@ private const val PARENT_VARIABLE = "parent_variable" @RunWith(RobolectricTestRunner::class) class RuntimeStoreTest { - private val evaluationContext = EvaluationContext(mock(), mock(), mock(), mock()) + private val evaluationContext = EvaluationContext(mock(), mock(), mock(), mock()) private val evaluator = mock { on { evaluationContext } doReturn evaluationContext } @@ -38,18 +39,19 @@ class RuntimeStoreTest { private val underTest = RuntimeStore(evaluator, errorCollector, div2Logger, divActionBinder) private var runtimeFromCallback: ExpressionsRuntime? = null - private val callback = ExpressionResolverImpl.OnCreateCallback { resolver, variableController -> + private val callback = ExpressionResolverImpl.OnCreateCallback { resolver, variableController, functionProvider -> runtimeFromCallback = ExpressionsRuntime( - resolver, variableController, null, underTest + resolver, variableController, null, functionProvider, underTest ) underTest.putRuntime(runtimeFromCallback!!) } private val resolver = ExpressionResolverImpl(mock(), evaluator, errorCollector, callback) private val rootVariableController: VariableController = mock() + private val functionProvider = mock() private val rootResolver = ExpressionResolverImpl(rootVariableController, evaluator, errorCollector, callback) private val rootRuntime: ExpressionsRuntime = - ExpressionsRuntime(rootResolver, rootVariableController, null, underTest) + ExpressionsRuntime(rootResolver, rootVariableController, null, functionProvider, underTest) @Before fun putRootResolver() { @@ -63,8 +65,8 @@ class RuntimeStoreTest { @Test fun `setPathToRuntimeWith links path to created runtime`() { - val newResolver = ExpressionResolverImpl(mock(), mock(), errorCollector, callback) - underTest.resolveRuntimeWith(path.fullPath, null, null, newResolver, newResolver) + val newResolver = ExpressionResolverImpl(mock(), evaluator, errorCollector, callback) + underTest.resolveRuntimeWith(path.fullPath, null, null, null, newResolver, newResolver) Assert.assertNotNull(underTest.getRuntimeWithOrNull(newResolver)) Assert.assertEquals( @@ -76,7 +78,7 @@ class RuntimeStoreTest { @Test fun `setPathToRuntimeWith creates runtime with new variables if variables provided`() { val variables = listOf(Variable.IntegerVariable(CHILD_VARIABLE, 123)) - underTest.resolveRuntimeWith(path.fullPath, variables, null, resolver, resolver) + underTest.resolveRuntimeWith(path.fullPath, variables, null, null, resolver, resolver) val runtime = underTest.getOrCreateRuntime(path.fullPath, null) Assert.assertNotNull(underTest.getRuntimeWithOrNull(resolver)) @@ -89,7 +91,7 @@ class RuntimeStoreTest { @Test fun `getOrCreateRuntime returns runtime for path if exist`() { val resolver = ExpressionResolverImpl(mock(), evaluator, errorCollector, callback) - val runtime = ExpressionsRuntime(resolver, mock(), null, underTest) + val runtime = ExpressionsRuntime(resolver, mock(), null, functionProvider, underTest) underTest.putRuntime(runtime, PATH, rootRuntime) Assert.assertEquals(runtime, underTest.getOrCreateRuntime(path.fullPath, null, null)) @@ -101,7 +103,7 @@ class RuntimeStoreTest { fun `getOrCreateRuntime returns parent runtime for path if no variables provided`() { val resolver = ExpressionResolverImpl(mock(), evaluator, errorCollector, callback) val parentVariableController = mock() - val runtime = ExpressionsRuntime(resolver, parentVariableController, null, underTest) + val runtime = ExpressionsRuntime(resolver, parentVariableController, null, functionProvider, underTest) underTest.putRuntime(runtime, PARENT_PATH, rootRuntime) Assert.assertEquals(runtime, underTest.getOrCreateRuntime(path.fullPath, parentResolver = resolver)) @@ -115,7 +117,7 @@ class RuntimeStoreTest { val parentVariableController = mock { on { getMutableVariable(PARENT_VARIABLE) } doReturn Variable.StringVariable(PARENT_VARIABLE, "123") } - val runtime = ExpressionsRuntime(resolver, parentVariableController, null, underTest) + val runtime = ExpressionsRuntime(resolver, parentVariableController, null, functionProvider, underTest) underTest.putRuntime(runtime, PARENT_PATH, rootRuntime) @@ -143,7 +145,7 @@ class RuntimeStoreTest { @Test fun `setPathToRuntimeWith links path to runtime`() { val resolver = ExpressionResolverImpl(mock(), evaluator, errorCollector, callback) - underTest.resolveRuntimeWith(path.fullPath, null, null, resolver, resolver) + underTest.resolveRuntimeWith(path.fullPath, null, null, null, resolver, resolver) Assert.assertNotNull(runtimeFromCallback) Assert.assertEquals( @@ -166,7 +168,7 @@ class RuntimeStoreTest { parentVariableController, evaluator, errorCollector, callback ) - underTest.resolveRuntimeWith(path.fullPath, variables, null, resolver, resolver) + underTest.resolveRuntimeWith(path.fullPath, variables, null, null, resolver, resolver) val newRuntime = underTest.getOrCreateRuntime(path.fullPath, null, null) Assert.assertNotNull(newRuntime) diff --git a/client/android/div/src/test/java/com/yandex/div/core/expression/variables/TwoWayVariableBinderTest.kt b/client/android/div/src/test/java/com/yandex/div/core/expression/variables/TwoWayVariableBinderTest.kt index 20cecfe00..ea6b337ac 100644 --- a/client/android/div/src/test/java/com/yandex/div/core/expression/variables/TwoWayVariableBinderTest.kt +++ b/client/android/div/src/test/java/com/yandex/div/core/expression/variables/TwoWayVariableBinderTest.kt @@ -3,6 +3,7 @@ package com.yandex.div.core.expression.variables import com.yandex.div.core.expression.ExpressionResolverImpl import com.yandex.div.core.expression.ExpressionsRuntime import com.yandex.div.core.expression.ExpressionsRuntimeProvider +import com.yandex.div.core.expression.FunctionProviderDecorator import com.yandex.div.core.expression.local.RuntimeStore import com.yandex.div.core.state.DivStatePath import com.yandex.div.core.view2.BindingContext @@ -10,6 +11,8 @@ import com.yandex.div.core.view2.Div2View import com.yandex.div.core.view2.errors.ErrorCollector import com.yandex.div.core.view2.errors.ErrorCollectors import com.yandex.div.data.Variable +import com.yandex.div.evaluable.EvaluationContext +import com.yandex.div.evaluable.Evaluator import com.yandex.div2.DivData import org.junit.Assert.assertEquals import org.junit.Test @@ -41,8 +44,12 @@ class TwoWayVariableBinderTest { } private val path = DivStatePath(0) private val store = RuntimeStore(mock(), mock(), mock(), mock()) - private val expressionResolver = ExpressionResolverImpl(mock(), mock(), mock(), mock()) - private val expressionsRuntime = ExpressionsRuntime(expressionResolver, variableController, mock(), store) + private val evaluationContext = EvaluationContext(mock(), mock(), mock(), mock()) + private val evaluator = mock { + on { evaluationContext } doReturn evaluationContext + } + private val expressionResolver = ExpressionResolverImpl(mock(), evaluator, mock(), mock()) + private val expressionsRuntime = ExpressionsRuntime(expressionResolver, variableController, mock(), mock(), store) private val expressionsRuntimeProvider = mock { on { getOrCreate(any(), any(), any()) } doReturn expressionsRuntime } diff --git a/client/android/div/src/test/java/com/yandex/div/core/view2/local/LocalVariablesTest.kt b/client/android/div/src/test/java/com/yandex/div/core/view2/local/LocalVariablesTest.kt index 3df1e545a..c6c57d328 100644 --- a/client/android/div/src/test/java/com/yandex/div/core/view2/local/LocalVariablesTest.kt +++ b/client/android/div/src/test/java/com/yandex/div/core/view2/local/LocalVariablesTest.kt @@ -124,7 +124,7 @@ class LocalVariablesTest { private fun setVariable(name: String, value: String, path: String) { val variableController = div2View.expressionsRuntime - ?.runtimeStore?.getOrCreateRuntime(path, null, null, null)?.variableController + ?.runtimeStore?.getOrCreateRuntime(path, null, null, null, null)?.variableController val variable = variableController?.getMutableVariable(name) ?: return variable.set(value) } diff --git a/schema/div-base.json b/schema/div-base.json index 4484c8c0b..01a03664e 100644 --- a/schema/div-base.json +++ b/schema/div-base.json @@ -222,6 +222,7 @@ }, "$description": "translations.json#/div_base_functions", "platforms": [ + "android", "ios" ] },